Bu makale akıllı sözleşme güvenliği konusunda mini bir kurs görevi görüyor ve Solidity akıllı sözleşmelerinde tekrarlanma eğiliminde olan sorunların ve güvenlik açıklarının kapsamlı bir listesini sunuyor. Bunlar kalite denetiminde ortaya çıkabilecek türden sorunlardır.
Solidity'deki bir güvenlik sorunu, akıllı sözleşmelerin amaçlandığı gibi davranmamasından kaynaklanıyor.
Fonlar çalınıyor
Fonların bir sözleşme kapsamında kilitlenmesi veya dondurulması
İnsanlar beklenenden daha az ödül alıyor (ödüller gecikiyor veya azaltılıyor)
İnsanlar beklenenden daha fazla ödül alıyor (enflasyona ve devalüasyona yol açıyor)
Yanlış gidebilecek her şeyin kapsamlı bir listesini yapmak mümkün değildir. Bununla birlikte, tıpkı geleneksel yazılım mühendisliğinin SQL enjeksiyonu, arabellek aşımları ve siteler arası komut dosyası oluşturma gibi ortak güvenlik açıkları temaları olması gibi, akıllı sözleşmelerin de belgelenebilen yinelenen anti-kalıpları vardır.
Bu kılavuzu daha çok bir referans olarak düşünün. Bunu bir kitaba dönüştürmeden her şeyi ayrıntılı olarak tartışmak mümkün değil (adil uyarı: bu makale 10k'den fazla kelime uzunluğundadır, bu nedenle onu yer imlerine eklemekten ve parçalar halinde okumaktan çekinmeyin). Ancak, nelere dikkat edilmesi ve nelerin çalışılması gerektiğinin bir listesi olarak hizmet eder. Bir konu size yabancı geliyorsa, bu durum, söz konusu güvenlik açığı sınıfını belirlemek için pratik yapmaya zaman ayırmaya değer olduğunun bir göstergesi olarak hizmet etmelidir.
Bu makale Solidity'de temel yeterliliğe sahip olduğunuzu varsaymaktadır. Solidity'de yeniyseniz lütfen ücretsiz Solidity eğitimimize bakın.
Akıllı sözleşmeye yeniden giriş hakkında kapsamlı bir şekilde yazdık, bu yüzden burada tekrarlamayacağız. Ama işte kısa bir özet:
Bir akıllı sözleşme başka bir akıllı sözleşmenin işlevini çağırdığında, ona Ether gönderdiğinde veya ona bir token aktardığında, yeniden giriş olasılığı ortaya çıkar.
Yeniden giriş muhtemelen en iyi bilinen akıllı sözleşme güvenlik açığı olmasına rağmen, vahşi ortamda meydana gelen saldırıların yalnızca küçük bir yüzdesini oluşturur. Güvenlik araştırmacısı Pascal Caversaccio (pcaveraccio), yeniden giriş saldırılarının github listesini güncel tutuyor. Nisan 2023 itibarıyla bu depoda 46 yeniden giriş saldırısı belgelendi.
Basit bir hata gibi görünse de, hassas bir işlevi kimin çağırabileceğine (eterin çekilmesi veya mülkiyetin değiştirilmesi gibi) kısıtlamalar koymayı unutmak şaşırtıcı derecede sık oluyor.
Bir değiştirici mevcut olsa bile, aşağıdaki örnekte require ifadesinin eksik olduğu gibi, değiştiricinin doğru şekilde uygulanmadığı durumlar olmuştur.
// DO NOT USE! modifier onlyMinter { minters[msg.sender] == true_; }
Yukarıdaki kod, bu denetimden elde edilen gerçek bir örnektir: https://code4rena.com/reports/2023-01-rabbithole/#h-01-bad-implementation-in-minter-access-control-for-rabbitholereceipt-and- tavşan deliği biletleri-sözleşmeleri
Erişim kontrolünün yanlış gitmesinin başka bir yolu da burada
function claimAirdrop(bytes32 calldata proof[]) { bool verified = MerkleProof.verifyCalldata(proof, merkleRoot, keccak256(abi.encode(msg.sender))); require(verified, "not verified"); require(alreadyClaimed[msg.sender], "already claimed"); _transfer(msg.sender, AIRDROP_AMOUNT); }
Bu durumda, "alreadyClaimed" hiçbir zaman true değerine ayarlanmaz, böylece talep sahibi işlevi birden çok kez çağırabilir.
Yetersiz erişim kontrolünün oldukça yeni bir örneği, bir ticaret botu (adres bu diziyle başladığı için 0xbad adıyla anılan) tarafından flash kredileri almaya yönelik korumasız bir işlevdi. Bir gün bir saldırgan, yalnızca flash kredi sağlayıcısının değil, herhangi bir adresin flash kredi alma işlevini çağırabileceğini fark edene kadar, bir milyon dolardan fazla kar elde etti.
Ticaret botlarında genellikle olduğu gibi, işlemleri yürütmek için kullanılan akıllı sözleşme kodu doğrulanmadı, ancak saldırgan yine de bu zayıf noktayı keşfetti. Daha fazla bilgi rekt haberlerinde.
Erişim kontrolü, bir işlevi kimin çağırdığını kontrol etmekle ilgiliyse, giriş doğrulama, sözleşmeyi neyle çağırdıklarını kontrol etmekle ilgilidir.
Bu genellikle uygun require deyimlerini yerine koymayı unutmaktan kaynaklanır.
İşte temel bir örnek:
contract UnsafeBank { mapping(address => uint256) public balances; // allow depositing on other's behalf function deposit(address for) public payable { balances += msg.value; } function withdraw(address from, uint256 amount) public { require(balances[from] <= amount, "insufficient balance"); balances[from] -= amount; msg.sender.call{value: amout}(""); } }
Yukarıdaki sözleşme, hesabınızda bulunan miktardan fazlasını çekmediğinizi kontrol eder ancak keyfi bir hesaptan para çekmenizi engellemez.
Sushiswap, harici bir işlevin parametrelerinden birinin temizlenmemesi nedeniyle bu tür bir saldırıyla karşılaştı.
Uygun olmayan erişim kontrolü, msg.sender'ın yeterli kısıtlamalara sahip olmadığı anlamına gelir. Uygun olmayan giriş doğrulaması, işleve ilişkin argümanların yeterince sterilize edilmediği anlamına gelir. Bu anti-modelin bir de tersi var: Bir işlev çağrısına çok fazla kısıtlama koymak.
Aşırı doğrulama muhtemelen fonların çalınmayacağı anlamına gelir, ancak fonların sözleşmeye kilitlenmesi anlamına da gelebilir. Çok fazla korumanın mevcut olması da iyi bir şey değil.
En dikkat çeken olaylardan biri, 34 milyon dolar değerinde Eth'in akıllı sözleşmeye sıkışıp çekilmesiyle sonuçlanan Akutars NFT'siydi.
Sözleşme, Hollanda açık artırma fiyatının üzerindeki tüm geri ödemeler verilinceye kadar sözleşme sahibinin sözleşmeden çekilmesini önleyecek iyi niyetli bir mekanizmaya sahipti. Ancak aşağıda bağlantısı verilen Twitter başlığında belgelenen bir hata nedeniyle, sahibi parayı çekemedi.
Sushiswap, güvenilmeyen kullanıcılara çok fazla güç verirken Akutars NFT, yöneticiye çok az güç verdi. Akıllı sözleşmeler tasarlarken, her bir kullanıcı sınıfına ne kadar özgürlük verilmesi gerektiğine ilişkin öznel bir yargı vardır ve bu karar, otomatik testlere ve araçlara bırakılamaz. Merkezi olmayan yönetim, güvenlik ve kullanıcı deneyimi arasında dikkate alınması gereken önemli ödünler var.
Akıllı sözleşme programcısı için, kullanıcıların belirli işlevlerle ne yapması ve ne yapmaması gerektiğini açıkça yazmak, geliştirme sürecinin önemli bir parçasıdır.
Aşırı güçlü yöneticiler konusunu daha sonra tekrar ele alacağız.
Giriş bölümünde belirtildiği gibi akıllı sözleşmelerin saldırıya uğramasının dört temel yolu vardır:
Burada “para” sadece kripto para birimini değil, token gibi değerli her şeyi ifade ediyor. Bir akıllı sözleşmeyi kodlarken veya denetlerken geliştiricinin, değerin sözleşmeye girip çıkması için amaçlanan yollar konusunda dikkatli olması gerekir. Yukarıda listelenen sorunlar, akıllı sözleşmelerin saldırıya uğramasının başlıca yollarıdır, ancak aşağıda belgelenen, büyük sorunlara yol açabilecek birçok başka temel neden de vardır.
Oylamayı tartmak için vanilya ERC20 tokenlerini veya NFT'leri bilet olarak kullanmak güvenli değildir çünkü saldırganlar bir adresle oy verebilir, tokenları başka bir adrese aktarabilir ve bu adresten tekrar oy kullanabilir.
İşte minimal bir örnek:
// A malicious voter can simply transfer their tokens to // another address and vote again. contract UnsafeBallot { uint256 public proposal1VoteCount; uint256 public proposal2VoteCount; IERC20 immutable private governanceToken; constructor(IERC20 _governanceToken) { governanceToken = _governanceToken; } function voteFor1() external notAlreadyVoted { proposal1VoteCount += governanceToken.balanceOf(msg.sender); } function voteFor2() external notAlreadyVoted { proposal2VoteCount += governanceToken.balanceOf(msg.sender); } // prevent the same address from voting twice, // however the attacker can simply // transfer to a new address modifier notAlreadyVoted { require(!alreadyVoted[msg.sender], "already voted"); _; alreadyVoted[msg.sender] = true; } }
Bu saldırıyı önlemek için ERC20 Snapshot veya ERC20 Votes kullanılmalıdır. Geçmişteki bir zamanın anlık görüntüsünü alarak, mevcut token bakiyeleri yasadışı oylama gücü elde etmek için manipüle edilemez.
Bununla birlikte, anlık görüntü veya oylama özelliğine sahip bir ERC20 tokenı kullanmak, eğer birisi bakiyesini geçici olarak artırmak için flash kredi alabilir ve ardından aynı işlemde bakiyesinin anlık görüntüsünü alabilirse sorunu tam olarak çözmez. Bu anlık görüntü oylama için kullanılırsa, ellerinde makul olmayan miktarda oy olacaktır.
Flashloan, bir adrese büyük miktarda Ether veya token ödünç verir, ancak para aynı işlemde geri ödenmezse geri alınır.
contract SimpleFlashloan { function borrowERC20Tokens() public { uint256 before = token.balanceOf(address(this)); // send tokens to the borrower token.transfer(msg.sender, amount); // hand control back to the borrower to // let them do something IBorrower(msg.sender).onFlashLoan(); // require that the tokens got returned require(token.balanceOf(address(this) >= before); } }
Bir saldırgan, teklifleri kendi lehine çevirmek ve/veya kötü niyetli bir şey yapmak için birdenbire çok sayıda oy kazanmak amacıyla flaş kredi kullanabilir.
Bu muhtemelen DeFi'ye yönelik en yaygın (veya en azından en yüksek profilli) saldırıdır ve yüz milyonlarca doların kaybedilmesine neden olur. İşte yüksek profilli olanların bir listesi .
Blockchain'deki bir varlığın fiyatı genellikle varlıklar arasındaki mevcut döviz kuru olarak hesaplanır. Örneğin, bir sözleşme şu anda 1 USDC ile 100 k9coin arasında işlem görüyorsa, k9coin'in fiyatının 0,01 USDC olduğunu söyleyebilirsiniz. Ancak fiyatlar genellikle alım ve satım baskısına tepki olarak hareket eder ve flaş krediler büyük alım ve satım baskısı yaratabilir.
Bir varlığın fiyatı hakkında başka bir akıllı sözleşmeyi sorgularken geliştiricinin çok dikkatli olması gerekir çünkü aradıkları akıllı sözleşmenin flaş kredi manipülasyonuna karşı bağışık olduğunu varsayarlar.
Bir adresin akıllı sözleşme olup olmadığını bayt kodu boyutuna bakarak “kontrol edebilirsiniz”. Dışarıdan sahip olunan hesaplarda (normal cüzdanlar) herhangi bir bayt kodu yoktur. İşte bunu yapmanın birkaç yolu
import "@openzeppelin/contracts/utils/Address.sol" contract CheckIfContract { using Address for address; function addressIsContractV1(address _a) { return _a.code.length == 0; } function addressIsContractV2(address _a) { // use the openzeppelin libraryreturn _a.isContract(); } }
Ancak bunun birkaç sınırlaması var
Genel olarak bir adresin bir sözleşme olup olmadığını kontrol etmek genellikle (ancak her zaman değil) bir anti-modeldir. Çoklu imza cüzdanları akıllı sözleşmelerdir ve çoklu imza cüzdanlarını bozabilecek herhangi bir şey yapmak, şekillendirilebilirliği bozar.
Bunun istisnası, transfer kancasını çağırmadan önce hedefin akıllı bir sözleşme olup olmadığını kontrol etmektir. Bu konuda daha sonra daha fazla bilgi vereceğiz.
Tx.origin'i kullanmak için nadiren iyi bir neden vardır. Göndereni tanımlamak için tx.origin kullanılırsa ortadaki adam saldırısı mümkündür. Kullanıcı kötü niyetli bir akıllı sözleşmeyi çağırmak için kandırılırsa, akıllı sözleşme ortalığı kasıp kavurmak için tx.origin'in sahip olduğu tüm yetkiyi kullanabilir.
Aşağıdaki alıştırmayı ve kodun üzerindeki yorumları göz önünde bulundurun.
contract Phish { function phishingFunction() public { // this fails, because this contract does not have approval/allowance token.transferFrom(msg.sender, address(this), token.balanceOf(msg.sender)); // this also fails, because this creates approval for the contract,// not the wallet calling this phishing function token.approve(address(this), type(uint256).max); } }
Bu, keyfi akıllı sözleşmeleri çağırırken güvende olduğunuz anlamına gelmez. Ancak çoğu protokolde, kimlik doğrulama için tx.origin kullanılırsa atlanacak bir güvenlik katmanı bulunur.
Bazen şuna benzeyen bir kod görebilirsiniz:
require(msg.sender == tx.origin, "no contracts");
Bir akıllı sözleşme başka bir akıllı sözleşmeyi çağırdığında, msg.sender akıllı sözleşme olacak ve tx.origin kullanıcının cüzdanı olacak, böylece gelen çağrının bir akıllı sözleşmeden geldiğine dair güvenilir bir gösterge verecektir. Çağrı yapıcıdan gelse bile bu doğrudur.
Çoğu zaman bu tasarım modeli iyi bir fikir değildir. Çoklu imza cüzdanları ve EIP 4337'deki Cüzdanlar, bu koda sahip bir işlevle etkileşimde bulunamayacaktır. Bu model, çoğu kullanıcının geleneksel bir cüzdan kullanmasını beklemenin makul olduğu NFT darphanelerinde yaygın olarak görülebilir. Ancak hesap soyutlama daha popüler hale geldikçe, bu model yardımcı olmaktan çok engel olacaktır.
Acı verici bir saldırı, bilgisayar korsanının, ekonomik olarak kazanç sağlamasalar bile, diğer insanlar için "üzüntü yaratmaya" çalıştığı anlamına gelir.
Akıllı bir sözleşme, sonsuz bir döngüye girerek kendisine iletilen tüm gazı kötü niyetli bir şekilde tüketebilir. Aşağıdaki örneği düşünün:
contract Mal { fallback() external payable { // infinite loop uses up all the gas while (true) { } } }
Başka bir sözleşme, Ethereum'u aşağıdaki gibi bir adres listesine dağıtıyorsa:
contract Distribute { funtion distribute(uint256 total) public nonReentrant { for (uint i; i < addresses.length; ) { (bool ok, ) addresses.call{value: total / addresses.length}(""); // ignore ok, if it reverts we move on // traditional gas saving trick for for loops unchecked { ++i; } } } }
Daha sonra fonksiyon Mal'a eter gönderdiğinde geri dönecektir. Yukarıdaki koddaki çağrı mevcut gazın 63/64'ünü iletir, bu nedenle gazın yalnızca 1/64'ü kaldığında işlemi tamamlamak için yeterli gaz muhtemelen olmayacaktır.
Akıllı bir sözleşme, çok fazla gaz tüketen geniş bir bellek dizisi döndürebilir
Aşağıdaki örneği göz önünde bulundurun
function largeReturn() public { // result might be extremely long! (book ok, bytes memory result) = otherContract.call(abi.encodeWithSignature("foo()")); require(ok, "call failed"); }
Bellek dizileri 724 bayttan sonra ikinci dereceden miktarda gaz tüketir, bu nedenle dikkatle seçilmiş bir dönüş veri boyutu arayan kişiyi üzebilir.
Sonuç değişkeni kullanılmasa bile yine de belleğe kopyalanır. Dönüş boyutunu belirli bir miktarla sınırlamak istiyorsanız montajı kullanabilirsiniz.
function largeReturn() public { assembly { let ok := call(gas(), destinationAddress, value, dataOffset, dataSize, 0x00, 0x00); // nothing is copied to memory until you // use returndatacopy() } }
Depolamayı silmek gaz açısından verimli bir işlem olmasına rağmen yine de net bir maliyeti vardır. Dizi çok uzarsa silinmesi imkansız hale gelir. İşte minimal bir örnek
contract VulnerableArray { address[] public stuff; function addSomething(address something) public { stuff.push(something); } // if stuff is too long, this will become undeletable due to // the gas cost function deleteEverything() public onlyOwner { delete stuff; } }
Akıllı bir sözleşme, aktarım kancalarına sahip tokenleri aktarırsa, saldırgan, tokenı kabul etmeyen bir sözleşme kurabilir (ya onReceive işlevi yoktur ya da işlevi geri dönecek şekilde programlar). Bu, jetonu devredilemez hale getirecek ve tüm işlemin geri alınmasına neden olacaktır.
SafeTransfer veya transfer'i kullanmadan önce, alıcının işlemi geri dönmeye zorlayabileceği olasılığını göz önünde bulundurun.
contract Mal is IERC721Receiver, IERC1155Receiver, IERC777Receiver { // this will intercept any transfer hook fallback() external payable { // infinite loop uses up all the gaswhile (true) { } } // we could also selectively deny transactions function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4) { if (wakeUpChooseViolence()) { revert(); } else { return IERC721Receiver.onERC721Received.selector; } } }
Blockchain üzerinde tek bir işlemle güvenli bir şekilde rastgelelik oluşturmak şu anda mümkün değil. Blok zincirlerinin tamamen deterministik olması gerekir, aksi takdirde dağıtılmış düğümler durum hakkında bir fikir birliğine varamaz. Tamamen deterministik oldukları için herhangi bir “rastgele” sayı tahmin edilebilir. Aşağıdaki zar atma işlevinden yararlanılabilir.
contract UnsafeDice { function randomness() internal returns (uint256) { return keccak256(abi.encode(msg.sender, tx.origin, block.timestamp, tx.gasprice, blockhash(block.number - 1); } // our dice can land on one of {0,1,2,3,4,5}function rollDice() public payable { require(msg.value == 1 ether); if (randomness() % 6) == 5) { msg.sender.call{value: 2 ether}(""); } } } contract ExploitDice { function randomness() internal returns (uint256) { return keccak256(abi.encode(msg.sender, tx.origin, block.timestamp, tx.gasprice, blockhash(block.number - 1); } function betSafely(IUnsafeDice game) public payable { if (randomness % 6) == 5)) { game.betSafely{value: 1 ether}() } // else don't do anything } }
Rastgeleliği nasıl ürettiğinizin bir önemi yoktur çünkü bir saldırgan bunu tam olarak kopyalayabilir. Msj.gönderici, zaman damgası vb. gibi daha fazla "entropi" kaynağı kullanmanın hiçbir etkisi olmayacaktır çünkü akıllı sözleşme bunu iki kez ölçebilir.
Chainlink, güvenli rastgele sayılar elde etmek için popüler bir çözümdür. Bunu iki adımda yapar. İlk olarak akıllı sözleşmeler kahine bir rastgelelik isteği gönderir, ardından birkaç blok sonra kahin rastgele bir sayıyla yanıt verir.
Saldırgan geleceği tahmin edemediğinden rastgele sayıyı da tahmin edemez.
Akıllı sözleşme kehaneti yanlış kullanmadığı sürece.
Chainlink'in fiyat kehanetlerini belirli bir zaman dilimi içinde güncel tutmasını sağlayacak bir SLA (hizmet seviyesi sözleşmesi) yoktur. Zincir ciddi şekilde tıkandığında (örneğin, Yuga Labs Otherside nanesinin Ethereum'u hiçbir işlem yapılmayacak kadar ağır basması gibi), fiyat güncellemeleri gecikebilir.
Fiyat kahini kullanan bir akıllı sözleşme, verilerin eski olup olmadığını, yani yakın zamanda belirli bir eşik dahilinde güncellendiğini açıkça kontrol etmelidir. Aksi halde fiyatlar konusunda güvenilir bir karar veremez.
Fiyatın bir sapma eşiğini aşmazsa kahinin gazdan tasarruf etmek için fiyatı güncellemeyebileceği ve bu durumun "eski" olarak kabul edilen zaman eşiğini etkileyebileceği ek bir komplikasyon daha vardır.
Akıllı sözleşmenin dayandığı kehanetin SLA'sını anlamak önemlidir.
Bir kehanet ne kadar güvenli görünürse görünsün gelecekte bir saldırı keşfedilebilir. Buna karşı tek savunma birden fazla bağımsız kahin kullanmaktır.
Blok zinciri oldukça güvenli olabilir, ancak ilk etapta veriyi zincire koymak, blok zincirinin sağladığı tüm güvenlik garantilerinden feragat eden bir tür zincir dışı operasyonu gerektirir. Kahinler dürüst kalsalar bile veri kaynakları manipüle edilebilir. Örneğin, bir kahin, merkezi bir borsanın fiyatlarını güvenilir bir şekilde raporlayabilir, ancak bunlar büyük alım ve satım emirleriyle manipüle edilebilir. Benzer şekilde, sensör verilerine veya bazı web2 API'lerine bağlı olan oracle'lar, geleneksel hackleme vektörlerine tabidir.
İyi bir akıllı sözleşme mimarisi, mümkün olduğunca oracle kullanımını tamamen önler.
Aşağıdaki sözleşmeyi düşünün
contract MixedAccounting { uint256 myBalance; function deposit() public payable { myBalance = myBalance + msg.value; } function myBalanceIntrospect() public view returns (uint256) { return address(this).balance; } function myBalanceVariable() public view returns (uint256) { return myBalance; } function notAlwaysTrue() public view returns (bool) { return myBalanceIntrospect() == myBalanceVariable(); } }
Yukarıdaki sözleşmenin alma veya geri dönüş işlevi yoktur, dolayısıyla Ether'in doğrudan ona aktarılması eski durumuna dönecektir. Ancak bir sözleşme, Ether'i kendi kendini yok ederek zorla gönderebilir.
Bu durumda myBalanceIntrospect(), myBalanceVariable()'dan büyük olacaktır. Ether muhasebe yöntemi iyidir, ancak her ikisini de kullanırsanız sözleşme tutarsız davranışlara sahip olabilir.
Aynı durum ERC20 tokenleri için de geçerlidir.
contract MixedAccountingERC20 { IERC20 token; uint256 myTokenBalance; function deposit(uint256 amount) public { token.transferFrom(msg.sender, address(this), amount); myTokenBalance = myTokenBalance + amount; } function myBalanceIntrospect() public view returns (uint256) { return token.balanceOf(address(this)); } function myBalanceVariable() public view returns (uint256) { return myTokenBalance; } function notAlwaysTrue() public view returns (bool) { return myBalanceIntrospect() == myBalanceVariable(); } }
Yine myBalanceIntrospect() ve myBalanceVariable() işlevlerinin her zaman aynı değeri döndüreceğini varsayamayız. ERC20 tokenlerini doğrudan MixedAccountingERC20'ye aktarmak, para yatırma işlevini atlamak ve myTokenBalance değişkenini güncellememek mümkündür.
Dengeleri iç gözlemle kontrol ederken, dengenin dışarıdan biri tarafından istenildiği gibi değiştirilebilmesi nedeniyle eşitlik kontrollerinin katı bir şekilde kullanılmasından kaçınılmalıdır.
Bu, Solidity'nin bir tuhaflığı değil, daha çok, adreslere özel ayrıcalıklar vermek için kriptografinin nasıl kullanılacağı konusunda geliştiriciler arasında yaygın bir yanlış anlamadır. Aşağıdaki kod güvenli değil
contract InsecureMerkleRoot { bytes32 merkleRoot; function airdrop(bytes[] calldata proof, bytes32 leaf) external { require(MerkleProof.verifyCalldata(proof, merkleRoot, leaf), "not verified"); require(!alreadyClaimed[leaf], "already claimed airdrop"); alreadyClaimed[leaf] = true; mint(msg.sender, AIRDROP_AMOUNT); } }
Şifreleme kanıtlarının (merkle ağaçları, imzalar vb.) msg.sender'a bağlanması gerekir; saldırganın özel anahtarı almadan bunu değiştiremeyeceği bir durumdur.
function limitedMultiply(uint8 a, uint8 b) public pure returns (uint256 product) { product = a * b; }
Ürün bir uint256 değişkeni olmasına rağmen çarpma sonucu 255'ten büyük olamaz, aksi takdirde kod eski durumuna dönecektir.
Bu sorun, her değişkenin ayrı ayrı yükseltilmesiyle azaltılabilir.
function unlimitedMultiply(uint8 a, uint8 b) public pure returns (uint256 product) { product = uint256(a) * uint256(b); }
Bir yapı içinde paketlenmiş tam sayıların çarpılması durumunda buna benzer bir durum ortaya çıkabilir. Bir yapı içinde paketlenmiş küçük değerleri çarparken buna dikkat etmelisiniz.
struct Packed { uint8 time; uint16 rewardRate } //... Packed p; p.time * p.rewardRate; // this might revert!
Sağlamlık, bir tamsayıyı daha küçük bir sayıya dönüştürmenin güvenli olup olmadığını kontrol etmez. Bazı iş mantıkları downcasting'in güvenli olduğunu garanti etmediği sürece SafeCast gibi bir kütüphane kullanılmalıdır.
function test(int256 value) public pure returns (int8) { return int8(value + 1); // overflows and does not revert }
Kod, myArray[1] içindeki verileri myArray[0]'a kopyalıyor gibi görünüyor, ancak kopyalamıyor. İşlevdeki son satırı yorumladığınızda derleyici, işlevin bir görünüm işlevine dönüştürülmesi gerektiğini söyleyecektir. Foo'ya yazma, temeldeki depolamaya yazmaz.
contract DoesNotWrite { struct Foo { uint256 bar; } Foo[] public myArray; function moveToSlot0() external { Foo storage foo = myArray[0]; foo = myArray[1]; // myArray[0] is unchanged // we do this to make the function a state // changing operation // and silence the compiler warning myArray[1] = Foo({bar: 100}); } }
Bu nedenle depolama işaretçilerine yazmayın.
Bir eşleme (veya dinamik dizi) bir yapının içindeyse ve yapı silinirse, eşleme veya dizi silinmeyecektir.
Bir diziyi silme dışında, delete anahtar sözcüğü yalnızca bir depolama yuvasını silebilir. Depolama yuvası başka depolama yuvalarına referanslar içeriyorsa bunlar silinmez.
contract NestedDelete { mapping(uint256 => Foo) buzz; struct Foo { mapping(uint256 => uint256) bar; } Foo foo; function addToFoo(uint256 i) external { buzz[i].bar[5] = 6; } function getFromFoo(uint256 i) external view returns (uint256) { return buzz[i].bar[5]; } function deleteFoo(uint256 i) external { // internal map still holds the data in the // mapping and array delete buzz[i]; } }
Unutmayın, Solidity'de haritalar asla "boş" değildir. Yani birisi silinmiş bir öğeye erişirse, işlem geri dönmeyecek, bunun yerine söz konusu veri türü için sıfır değerini döndürecektir.
Yalnızca güvenilir ERC20 tokenlarıyla çalışıyorsanız bu sorunların çoğu geçerli değildir. Ancak keyfi veya kısmen güvenilmeyen bir ERC20 tokenıyla etkileşime girerken dikkat etmeniz gereken bazı noktalar var.
Güvenilmeyen tokenlarla uğraşırken bakiyenizin mutlaka miktar kadar artacağını varsaymamalısınız. Bir ERC20 tokeninin transfer fonksiyonunu aşağıdaki şekilde uygulaması mümkündür:
contract ERC20 { // internally called by transfer() and transferFrom() // balance and approval checks happen in the caller function _transfer(address from, address to, uint256 amount) internal returns (bool) { fee = amount * 100 / 99; balanceOf[from] -= to; balanceOf[to] += (amount - fee); balanceOf[TREASURY] += fee; emit Transfer(msg.sender, to, (amount - fee)); return true; } }
Bu token her işleme %1 vergi uygular. Yani akıllı bir sözleşme token ile aşağıdaki gibi etkileşime girerse ya beklenmedik geri dönüşler alırız ya da para çalınır.
contract Stake { mapping(address => uint256) public balancesInContract; function stake(uint256 amount) public { token.transferFrom(msg.sender, address(this), amount); balancesInContract[msg.sender] += amount; // THIS IS WRONG! } function unstake() public { uint256 toSend = balancesInContract[msg.sender]; delete balancesInContract[msg.sender]; // this could revert because toSend is 1% greater than// the amount in the contract. Otherwise, 1% will be "stolen"// from other depositors. token.transfer(msg.sender, toSend); } }
Yeniden temellendirme tokeni, Olympus DAO'nun sOhm token'ı ve Ampleforth'un AMPL token'ı ile popüler hale getirildi. Coingecko, ERC20 tokenlarının yeniden temellendirilmesinin bir listesini tutuyor.
Bir token yeniden baz aldığında, toplam arz değişir ve yeniden taban yönüne bağlı olarak herkesin bakiyesi artar veya azalır.
Bir yeniden temellendirme jetonuyla uğraşırken aşağıdaki kodun bozulması muhtemeldir
contract WillBreak { mapping(address => uint256) public balanceHeld; IERC20 private rebasingToken function deposit(uint256 amount) external { balanceHeld[msg.sender] = amount; rebasingToken.transferFrom(msg.sender, address(this), amount); } function withdraw() external { amount = balanceHeld[msg.sender]; delete balanceHeld[msg.sender]; // ERROR, amount might exceed the amount // actually held by the contract rebasingToken.transfer(msg.sender, amount); } }
Birçok sözleşmenin çözümü, tokenlerin yeniden temellendirilmesine izin vermemektir. Ancak, hesap bakiyesini gönderene aktarmadan önce BalanceOf(address(this)) öğesini kontrol etmek için yukarıdaki kod değiştirilebilir. O zaman denge değişse bile hala işe yarayacaktır.
ERC20, standarda göre uygulanırsa, ERC20 tokenlerinin transfer kancaları yoktur ve dolayısıyla transfer ve transferFrom'da yeniden giriş sorunu yaşanmaz.
Transfer kancalarına sahip tokenlerin önemli avantajları vardır, bu nedenle tüm NFT standartları bunları uygular ve ERC777'nin nihai hale getirilmesinin nedeni budur. Ancak Openzeppelin'in ERC777 kütüphanesini kullanımdan kaldırması yeterince kafa karışıklığına neden oldu.
Protokolünüzün ERC20 belirteçleri gibi davranan ancak aktarım kancalarına sahip belirteçlerle uyumlu olmasını istiyorsanız, o zaman transfer ve transferFrom işlevlerini, alıcıya bir işlev çağrısı yayınlayacakmış gibi ele almak basit bir meseledir.
Bu ERC777 yeniden girişi Uniswap'in başına geldi (merak ediyorsanız Openzeppelin bu istismarı burada belgeledi).
ERC20 spesifikasyonu, bir transfer başarılı olduğunda ERC20 tokeninin true değerini döndürmesi gerektiğini belirtir. Çoğu ERC20 uygulaması, ödenek yetersiz olmadığı veya aktarılan miktar çok fazla olmadığı sürece başarısız olamayacağından çoğu geliştirici, ERC20 tokenlerinin dönüş değerini göz ardı etmeye ve başarısız bir aktarımın geri alınacağını varsaymaya alıştı.
Açıkçası, yalnızca davranışını bildiğiniz güvenilir bir ERC20 tokenıyla çalışıyorsanız bu önemli değildir. Ancak rastgele ERC20 tokenlarıyla uğraşırken davranıştaki bu farklılığın hesaba katılması gerekir.
Birçok sözleşmede, başarısız transferlerin false döndürmek yerine her zaman geri alınması gerektiğine dair örtülü bir beklenti vardır çünkü çoğu ERC20 tokeninin false döndürme mekanizması yoktur, bu nedenle bu çok fazla kafa karışıklığına yol açmıştır.
Bu konuyu daha da karmaşık hale getiren şey, bazı ERC20 tokenlarının, özellikle de Tether'in, true döndürme protokolünü takip etmemesidir. Bazı tokenler transfer başarısızlığı durumunda geri döner, bu da geri dönüşün arayan tarafa yansımasına neden olur. Bu nedenle bazı kütüphaneler, geri dönüşü engellemek ve bunun yerine bir boole döndürmek için ERC20 token transfer çağrılarını sarar.
Solady SafeTransfer (gaz açısından çok daha verimli)
Bu bir akıllı sözleşme güvenlik açığı değildir, ancak bütünlüğü sağlamak için burada bundan bahsediyoruz.
Şartname gereği sıfır ERC20 tokeninin aktarılmasına izin verilmektedir. Bu, ön uç uygulamalarda kafa karışıklığına yol açabilir ve kullanıcıları yakın zamanda kime token gönderdikleri konusunda kandırabilir. Metamask'ta bu konuyla ilgili daha fazla bilgi var.
(Web3 dilinde "sağlam", "halının altınızdan çekilmesi" anlamına gelir.)
Birisinin bir ERC20 tokenına, dilediği zaman token oluşturmasına, aktarmasına ve yakmasına veya kendi kendini yok etmesine veya yükseltmesine olanak tanıyan bir işlev eklemesini engelleyen hiçbir şey yoktur. Yani temelde bir ERC20 tokeninin ne kadar "güvenilmez" olabileceğinin bir sınırı vardır.
Borç verme ve borç alma tabanlı DeFi protokollerinin nasıl bozulabileceğini düşünürken, yazılım düzeyinde yayılan ve iş mantığı düzeyini etkileyen hataları düşünmek faydalı olacaktır. Bir tahvil sözleşmesi oluşturmak ve kapatmak için birçok adım vardır. İşte dikkate alınması gereken bazı saldırı vektörleri.
Teminatın protokolden çekilmesi durumunda, borçlunun krediyi geri ödeme teşviki olmadığından hem borç veren hem de borçlu kaybeder ve borçlu anaparayı kaybeder.
Yukarıda görülebileceği gibi, bir DeFi protokolünün "saldırıya uğraması", protokolden bir miktar paranın çekilmesinden (genellikle haber konusu olan türden olaylar) çok daha fazla düzeyde vardır. Bu, CTF (bayrağı ele geçirme) güvenlik tatbikatlarının yanıltıcı olabileceği alandır. Protokol fonlarının çalınması en feci sonuç olmasına rağmen, savunulması gereken tek sonuç kesinlikle bu değildir.
Harici bir akıllı sözleşmeyi çağırmanın iki yolu vardır: 1) işlevi bir arayüz tanımıyla çağırmak; 2) .call yöntemini kullanarak. Bu aşağıda gösterilmiştir
contract A { uint256 public x; function setx(uint256 _x) external { require(_x > 10, "x must be bigger than 10"); x = _x; } } interface IA { function setx(uint256 _x) external; } contract B { function setXV1(IA a, uint256 _x) external { a.setx(_x); } function setXV2(address a, uint256 _x) external { (bool success, ) = a.call(abi.encodeWithSignature("setx(uint256)", _x)); // success is not checked! } }
B sözleşmesinde, _x 10'dan küçükse setXV2 sessizce başarısız olabilir. Bir işlev .call yöntemiyle çağrıldığında, aranan kişi geri dönebilir ancak ebeveyn geri dönmez. Başarı değeri kontrol edilmeli ve kod davranışı buna göre dallanmalıdır.
Özel değişkenler hala blok zincirinde görünür olduğundan hassas bilgiler asla orada saklanmamalıdır. Eğer erişilebilir olmasaydı, doğrulayıcılar değerlerine bağlı olan işlemleri nasıl gerçekleştirebileceklerdi? Özel değişkenler dışarıdan bir Solidity sözleşmesinden okunamaz, ancak bir Ethereum istemcisi kullanılarak zincir dışında okunabilirler.
Bir değişkeni okumak için onun depolama yuvasını bilmeniz gerekir. Aşağıdaki örnekte myPrivateVar'ın depolama yuvası 0'dır.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract PrivateVarExample { uint256 private myPrivateVar; constructor(uint256 _initialValue) { myPrivateVar = _initialValue; } }
Dağıtılan akıllı sözleşmenin özel değişkenini okumak için javascript kodu
const Web3 = require("web3"); const PRIVATE_VAR_EXAMPLE_ADDRESS = "0x123..."; // Replace with your contract address async function readPrivateVar() { const web3 = new Web3("http://localhost:8545"); // Replace with your provider's URL // Read storage slot 0 (where 'myPrivateVar' is stored) const storageSlot = 0; const privateVarValue = await web3.eth.getStorageAt( PRIVATE_VAR_EXAMPLE_ADDRESS, storageSlot ); console.log("Value of private variable 'myPrivateVar':", web3.utils.hexToNumberString(privateVarValue)); } readPrivateVar();
Delege çağrısı, tüm kontrolü delege edilen kişiye devrettiği için asla güvenilmeyen sözleşmelerle kullanılmamalıdır. Bu örnekte güvenilmeyen sözleşme, sözleşmedeki tüm eterleri çalıyor.
contract UntrustedDelegateCall { constructor() payable { require(msg.value == 1 ether); } function doDelegateCall(address _delegate, bytes calldata data) public { (bool ok, ) = _delegate.delegatecall(data); require(ok, "delegatecall failed"); } } contract StealEther { function steal() public { // you could also selfdestruct here // if you really wanted to be mean (bool ok,) = tx.origin.call{value: address(this).balance}(""); require(ok); } function attack(address victim) public { UntrustedDelegateCall(victim).doDelegateCall( address(this), abi.encodeWithSignature("steal()")); } }
Bu konunun hakkını tek bir bölümde veremeyiz. Çoğu yükseltme hatası genellikle Openzeppelin'in kask eklentisini kullanarak ve hangi sorunlara karşı koruduğunu okuyarak önlenebilir. ( https://docs.openzeppelin.com/upgrades-plugins/1.x/ ).
Bir sözleşmenin bir sahibinin veya yöneticisinin olması, onların yetkilerinin sınırsız olması gerektiği anlamına gelmez. Bir NFT'yi düşünün. NFT satışından elde edilen kazançları yalnızca sahibin çekmesi mantıklıdır, ancak sahibinin özel anahtarları ele geçirilirse sözleşmeyi duraklatabilmek (blok transferler) büyük zarara yol açabilir. Genel olarak, gereksiz riski en aza indirmek için yönetici ayrıcalıklarının mümkün olduğu kadar az olması gerekir.
Sözleşmeli mülkiyetten bahsetmişken…
Bu teknik olarak bir güvenlik açığı değildir ancak OpenZeppelin'e sahip olunabilir , mülkiyetin var olmayan bir adrese devredilmesi durumunda sözleşme mülkiyetinin kaybına neden olabilir. Ownable2step, alıcının mülkiyeti onaylamasını gerektirir. Bu, sahipliğin yanlışlıkla yanlış yazılmış bir adrese gönderilmesine karşı sigorta sağlar.
Sağlamlığın kayan noktası yoktur, dolayısıyla yuvarlama hataları kaçınılmazdır. Tasarımcı, yapılacak doğru şeyin yukarıya mı yoksa aşağı yuvarlama mı olduğunun ve yuvarlamanın kimin lehine olması gerektiğinin bilincinde olmalıdır.
Bölme işlemi her zaman en son yapılmalıdır. Aşağıdaki kod, farklı sayıda ondalık sayıya sahip stabilcoin'ler arasında hatalı dönüştürme işlemi yapıyor. Aşağıdaki takas mekanizması, kullanıcının dai (18 ondalık rakamı olan) ile takas ederken küçük miktarda USDC'yi (6 ondalık rakamı olan) ücretsiz olarak almasına olanak tanır. daiToTake değişkeni sıfıra yuvarlanır ve sıfırdan farklı bir usdcAmount karşılığında kullanıcıdan hiçbir şey almaz.
contract Exchange { uint256 private constant CONVERSION = 1e12; function swapDAIForUSDC(uint256 usdcAmount) external pure returns (uint256 a) { uint256 daiToTake = usdcAmount / CONVERSION; conductSwap(daiToTake, usdcAmount); } }
Etheruem (ve benzeri zincirler) bağlamında önden koşmak, bekleyen bir işlemi gözlemlemek ve daha yüksek bir gas fiyatı ödeyerek ondan önce başka bir işlem gerçekleştirmek anlamına gelir. Yani saldırgan işlemin "önüne geçmiştir". İşlem karlı bir işlemse, daha yüksek bir gas fiyatı ödemek dışında işlemi aynen kopyalamak mantıklı olacaktır. Bu olguya bazen madencinin çıkarabileceği değer anlamına gelen MEV adı verilir, ancak bazen diğer bağlamlarda maksimum çıkarılabilir değer anlamına gelir. Blok üreticileri, işlemleri yeniden sıralamak ve kendi işlemlerini eklemek için sınırsız güce sahiptir ve tarihsel olarak blok üreticileri, Ethereum'un hisse kanıtına gitmesinden önce madenciydi, dolayısıyla adı da buradan gelmektedir.
Ether'i akıllı bir sözleşmeden çekmek "karlı bir ticaret" olarak değerlendirilebilir. Sıfır maliyetli bir işlem gerçekleştirirsiniz (gaz dışında) ve sonunda başladığınızdan daha fazla kripto para birimine sahip olursunuz.
contract UnprotectedWithdraw { constructor() payable { require(msg.value == 1 ether, "must create with 1 eth"); } function unsafeWithdraw() external { (bool ok, ) = msg.sender.call{value: address(this).value}(""); require(ok, "transfer failed"). } }
Bu sözleşmeyi dağıtır ve geri çekilmeye çalışırsanız, öncü bir bot, bellek havuzundaki "unsafeWithdraw" çağrınızı fark edecek ve önce Ether'i almak için onu kopyalayacaktır.
ERC4626 eğitimimizde ERC-4626 enflasyon saldırısı hakkında derinlemesine yazdık. Ancak bunun özü, bir ERC4626 sözleşmesinin, bir tüccarın katkıda bulunduğu "varlıkların" yüzdesine göre "hisse" tokenlarını dağıtmasıdır.
Kabaca şu şekilde çalışır:
function getShares(...) external { // code shares_received = assets_contributed / total_assets; // more code }
Elbette hiç kimse varlıklara katkıda bulunmayacak ve hisseleri geri alamayacak, ancak birisinin hisseleri almak için ticareti ön plana çıkarması durumunda bunun olacağını tahmin edemezler.
Mesela havuzda 20 varlık varken 200 varlık katkıda bulunuyorlar, 100 hisse almayı bekliyorlar. Ancak birisi 200 varlık yatırmak için işlemi önden yürütürse formül 200/220 olacaktır, bu da sıfıra yuvarlanır ve kurbanın varlıklarını kaybetmesine ve sıfır hisse geri almasına neden olur.
Bunu soyut olarak anlatmak yerine gerçek bir örnekle anlatmak en doğrusu
Artık Eve'in 100 veya 50 yerine 150 jetonu var. Bunun çözümü, güvenilmeyen onaylarla uğraşırken onayı artırmadan veya azaltmadan önce sıfıra ayarlamaktır.
Bir varlığın fiyatı, alış ve satış baskısına tepki olarak hareket eder. Bellek havuzunda büyük bir emir duruyorsa, tüccarların emri kopyalamaya yönelik bir teşviki olur, ancak bu daha yüksek bir gas fiyatıyla olur. Bu şekilde varlığı satın alırlar, büyük siparişin fiyatı yukarı taşımasına izin verirler ve ardından hemen satarlar. Satış emrine bazen "geri dönüş" adı verilir. Satış emri, daha düşük gas fiyatına sahip bir satış emri verilerek yapılabilir, böylece sıra şu şekilde görünür
Bu saldırıya karşı birincil savunma, bir “kayma” parametresi sağlamaktır. Eğer "önden alım", fiyatı belirli bir eşiğin üzerine çıkarırsa, "büyük satın alma" emri geri dönecek ve öndekinin işlemde başarısız olmasına neden olacaktır.
Buna sandviç denir, çünkü büyük alımlar ön alım ve geri satışlarla gölgelenir. Bu saldırı aynı zamanda tam tersi yönde büyük satış emirlerinde de işe yarar.
Ön koşu çok büyük bir konudur. Flashbots konuyu kapsamlı bir şekilde araştırdı ve olumsuz dışsallıkları en aza indirmeye yardımcı olacak çeşitli araçlar ve araştırma makaleleri yayınladı.
Önden koşmanın uygun blockchain mimarisiyle "tasarlanıp tasarlanamayacağı" henüz kesin olarak çözülmemiş bir tartışma konusudur. Aşağıdaki iki makale konuyla ilgili kalıcı klasiklerdir:
Ethereum karanlık bir ormandır
Akıllı sözleşmeler bağlamında dijital imzaların iki kullanımı vardır:
Bir kullanıcıya NFT oluşturma ayrıcalığını vermek için dijital imzaları güvenli bir şekilde kullanmanın bir örneğini burada bulabilirsiniz:
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract NFT is ERC721("name", "symbol") { function mint(bytes calldata signature) external { address recovered = keccak256(abi.encode(msg.sender)).toEthSignedMessageHash().recover(signature); require(recovered == authorizer, "signature does not match"); } }
Klasik bir örnek, ERC20'deki Onayla işlevidir. Hesabımızdan belirli miktarda token çekmek üzere bir adresi onaylamak için, gaza mal olan gerçek bir Ethereum işlemi yapmamız gerekiyor.
Bazen alıcıya zincir dışı bir dijital imza iletmek daha verimli olabilir, ardından alıcı, işlemi yürütme yetkisine sahip olduğunu kanıtlamak için imzayı akıllı sözleşmeye iletir.
ERC20Permit, onayların dijital imzayla yapılmasını sağlar. İşlev şu şekilde açıklanmaktadır:
function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public
Gerçek bir onay işlemi göndermek yerine, sahibi, harcamayı yapan kişi için onayı (son tarihle birlikte) "imzalayabilir". Onaylanan harcamacı daha sonra sağlanan parametrelerle izin fonksiyonunu çağırabilir.
v, r ve s değişkenlerini sık sık göreceksiniz. Sırasıyla uint8, bytes32 ve bytes32 veri türleriyle sağlam bir şekilde temsil edilirler. Bazen imzalar, tüm bu değerlerin abi.encodePacked(r, s, v); olarak bir araya getirildiği 65 baytlık bir dizi olarak temsil edilir;
Bir imzanın diğer iki temel bileşeni mesaj karması (32 bayt) ve imzalama adresidir. Sıra şuna benziyor
Genel bir adres (ethAddress) oluşturmak için özel bir anahtar (privKey) kullanılır
Akıllı bir sözleşme ethAddress'i önceden saklar
Zincir dışı bir kullanıcı bir mesaja hash uygular ve hash'i imzalar. Bu, msgHash çiftini ve (r, s, v) imzasını üretir.
Akıllı sözleşme bir mesaj alır, msgHash üretmek için onu hashler ve ardından hangi adresin çıktığını görmek için onu (r, s, v) ile birleştirir.
Adres ethAddress ile eşleşiyorsa imza geçerlidir (bazı varsayımlara göre, bunu yakında göreceğiz!)
Akıllı sözleşmeler, kombinasyon dediğimiz şeyi yapmak ve adresi geri almak için 4. adımda önceden derlenmiş sözleşme ecrecover'ı kullanır.
Bu süreçte işlerin ters gidebileceği pek çok adım var.
Başlatılmamış bir değişken ecrecover çıktısıyla karşılaştırıldığında bu durum bir güvenlik açığına yol açabilir.
Bu kod savunmasızdır
contract InsecureContract { address signer; // defaults to address(0) // who lets us give the beneficiary the airdrop without them// spending gas function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { // ecrecover returns address(0) if the signature is invalid require(signer == ecrecover(keccak256(abi.encode(who, amount)), v, r, s), "invalid signature"); mint(msg.sender, AIRDROP_AMOUNT); } }
İmzanın tekrar oynatılması, bir sözleşmenin bir imzanın daha önce kullanılıp kullanılmadığını takip etmemesi durumunda gerçekleşir. Aşağıdaki kodda önceki sorunu düzelttik ancak hala güvenli değil.
contract InsecureContract { address signer; function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { address recovered == ecrecover(keccak256(abi.encode(who, amount)), v, r, s); require(recovered != address(0), "invalid signature"); require(recovered == signer, "recovered signature not equal signer"); mint(msg.sender, amount); } }
İnsanlar airdrop'u istedikleri kadar talep edebilirler!
Aşağıdaki satırları ekleyebiliriz
bytes memory signature = abi.encodePacked(v, r, s); require(!used[signature], "signature already used"); // mapping(bytes => bool); used[signature] = true;
Ne yazık ki kod hâlâ güvenli değil!
Geçerli bir imza verildiğinde, saldırgan farklı bir imza elde etmek için hızlı bir aritmetik işlemi yapabilir. Saldırgan daha sonra bu değiştirilmiş imzayı "tekrar oynatabilir". Ancak önce, geçerli bir imzayla başlayabileceğimizi, onu değiştirebileceğimizi ve yeni imzanın hâlâ geçerli olduğunu gösterebileceğimizi gösteren bazı kodlar verelim.
contract Malleable { // v = 28 // r = 0xf8479d94c011613baeffe9239e4ff65e2adbac744c34217ca7d51378e72c5204 // s = 0x57af17590a914b759c45aaeabaf513d5ef72d7da1bdd19d9f2e1bc371ece5b86 // m = 0x0000000000000000000000000000000000000000000000000000000000000003 function foo(bytes calldata msg, uint8 v, bytes32 r, bytes32 s) public pure returns (address, address){ bytes32 h = keccak256(msg); address a = ecrecover(h, v, r, s); // The following is math magic to invert the // signature and create a valid one // flip s bytes32 s2 = bytes32(uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141) - uint256(s)); // invert v uint8 v2; require(v == 27 || v == 28, "invalid v"); v2 = v == 27 ? 28 : 27; address b = ecrecover(h, v2, r, s2); assert(a == b); // different signatures, same address!; return (a, b); } }
Bu nedenle, çalışan örneğimiz hala savunmasızdır. Birisi geçerli bir imza sunduğunda, onun ayna görüntüsü imzası üretilebilir ve kullanılan imza kontrolü atlanabilir.
contract InsecureContract { address signer; function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { address recovered == ecrecover(keccak256(abi.encode(who, amount)), v, r, s); require(recovered != address(0), "invalid signature"); require(recovered == signer, "recovered signature not equal signer"); bytes memory signature = abi.encodePacked(v, r, s); require(!used[signature], "signature already used"); // this can be bypassed used[signature] = true; mint(msg.sender, amount); } }
Muhtemelen bu noktada güvenli bir imza kodu istiyorsunuz, değil mi? Sağlamlıkta imzalar oluşturma ve bunları dökümhanede test etme konusundaki eğitimimize sizi yönlendiriyoruz.
Ama işte kontrol listesi.
Eğer zincirde hash işlemi yapılmazsa yukarıdaki saldırı daha da genelleştirilebilir. Yukarıdaki örneklerde hash işlemi akıllı sözleşmede yapılmıştır, dolayısıyla yukarıdaki örnekler aşağıdaki istismarlara karşı savunmasız değildir.
İmzaları kurtarma koduna bakalım
// this code is vulnerable! function recoverSigner(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public returns (address signer) { require(signer == ecrecover(hash, v, r, s), "signer does not match"); // more actions }
Kullanıcı hem hash'i hem de imzaları sağlar. Saldırgan, imzalayan kişinin geçerli bir imzasını zaten görmüşse, başka bir iletinin karma değerini ve imzasını yeniden kullanabilir.
Bu nedenle mesajın zincir dışında değil akıllı sözleşmede hashlenmesi çok önemlidir.
Bu istismarı çalışırken görmek için Twitter'da yayınladığımız CTF'ye bakın.
1. Bölüm: https://twitter.com/RareSkills_io/status/1650869999266037760
2. Bölüm: https://twitter.com/RareSkills_io/status/1650897671543197701
https://twitter.com/RareSkills_io/status/1651527648676573185 https://twitter.com/RareSkills_io/status/1651224817465540611
İmzalar kullanıcıları tanımlamak için kullanılmamalıdır. Dövülebilirlik nedeniyle benzersiz oldukları varsayılamaz. Msg.sender çok daha güçlü benzersizlik garantilerine sahiptir.
Twitter'da düzenlediğimiz bir güvenlik tatbikatını burada görebilirsiniz. Bir kod tabanını denetlerken, bir hatanın mevcut olup olmadığını görmek için Solidity sürümünü Solidity sayfasındaki sürüm duyurularıyla karşılaştırın.
Akıllı sözleşmeler Proxy Modeli (veya daha nadiren metamorfik model) ile yükseltilebilir. Akıllı sözleşmeler, değişmeden kalmak için keyfi bir akıllı sözleşmenin işlevselliğine dayanmamalıdır.
Sağlamlık fonksiyonları transfer ve send kullanılmamalıdır. İşlemle iletilen gaz miktarını kasıtlı olarak 2.300 ile sınırlıyorlar, bu da çoğu operasyonun yakıtının bitmesine neden olacak.
Yaygın olarak kullanılan gnosis güvenli çoklu imza cüzdanı, geri dönüş işlevinde çağrının başka bir adrese iletilmesini destekler. Birisi çoklu imzalı cüzdana Ether göndermek için aktarma veya göndermeyi kullanırsa, geri dönüş işlevinin gazı tükenebilir ve aktarım başarısız olabilir. Gnosis güvenli geri dönüş fonksiyonunun ekran görüntüsü aşağıda verilmiştir. Okuyucu, 2300 gazını tüketmek için gereğinden fazla işlemin olduğunu açıkça görebilir.
Aktarma ve göndermeyi kullanan bir sözleşmeyle etkileşime girmeniz gerekiyorsa depolama ve sözleşme erişim işlemlerinin gas maliyetini azaltmanıza olanak tanıyan Ethereum erişim listesi işlemleri hakkındaki makalemize bakın.
Solidity 0.8.0'da yerleşik taşma ve taşma koruması bulunur. Yani kontrol edilmemiş bir blok olmadığı veya Yul'da düşük seviyeli kod kullanılmadığı sürece taşma tehlikesi yoktur. Bu nedenle ekstra kontrollerde gaz israfına neden olacağından SafeMath kütüphaneleri kullanılmamalıdır.
Block.timestamp'in madenciler tarafından manipüle edilebilmesi nedeniyle bir güvenlik açığı vektörü olduğunu belirten bazı literatür belgeleri. Bu genellikle zaman damgalarının bir rastgelelik kaynağı olarak kullanılması için geçerlidir ve daha önce belgelendiği gibi bunun yapılmaması gerekir. Birleştirme sonrası Ethereum, zaman damgasını tam olarak 12 saniyelik (veya 12 saniyenin katları) aralıklarla günceller. Bununla birlikte, ikinci düzey ayrıntı düzeyinde zaman ölçümü bir anti-örüntüdür. Bir dakika ölçeğinde, bir doğrulayıcının blok yuvasını kaçırması ve blok üretiminde 24 saniyelik bir boşluk oluşması durumunda önemli miktarda hata olasılığı vardır.
Köşe vakaları kolayca tanımlanamaz, ancak yeterince gördükten sonra onlar için bir sezgi geliştirmeye başlarsınız. Köşe vakası, birinin ödül almaya çalışması ama hiçbir şeyi riske atmaması gibi bir şey olabilir. Bu geçerli, onlara sıfır ödül vermeliyiz. Benzer şekilde, genellikle ödülleri eşit olarak bölmek isteriz, ancak ya yalnızca tek bir alıcı varsa ve teknik olarak hiçbir bölünme olmayacaksa?
Bu örnek Akshay Srivastav'ın twitter başlığından alınmış ve değiştirilmiştir.
Bir kişinin ayrıcalıklı bir eylemi, bir dizi ayrıcalıklı adresin imza sağlaması durumunda gerçekleştirebildiği durumu düşünün.
contract VulnerableMultisigAuthorization { struct Authorization { bytes signature; address authorizer; bytes32 hashOfAction; // more fields } // more codef unction takeAction(Authorization[] calldata auths, bytes calldata action) public { // logic for avoiding replay attacks for (uint256 i; i < auths.length; ++i) { require(validateSignature(auths[i].signature, auths[i].authorizer), "invalid signature"); require(authorizers[auths[i].authorizer], "address is not an authorizer"); } doTheAction(action) } }
İmzalardan herhangi biri geçerli değilse veya imzalar geçerli bir adresle eşleşmezse geri dönüş gerçekleşir. Peki ya dizi boşsa? Bu durumda, herhangi bir imzaya ihtiyaç duymadan doTheAction'a kadar tüm yol boyunca atlayacaktır.
contract ProportionalRewards { mapping(address => uint256) originalId; address[] stakers; function stake(uint256 id) public { nft.transferFrom(msg.sender, address(this), id); stakers.append(msg.sender); } function unstake(uint256 id) public { require(originalId[id] == msg.sender, "not the owner"); removeFromArray(msg.sender, stakers); sendRewards(msg.sender, totalRewardsSinceLastclaim() / stakers.length()); nft.transferFrom(address(this), msg.sender, id); } }
Yukarıdaki kod tüm fonksiyon uygulamalarını göstermese de, fonksiyonlar adlarının tanımladığı gibi davransa bile hala bir hata var. Fark edebiliyor musun? Aşağı kaydırmadan önce cevabı görmemeniz için size biraz yer sağlayacak bir resim.
RemoveFromArray ve sendRewards işlevi yanlış sırada. Staker dizisinde tek kullanıcı olması durumunda sıfıra bölme hatası oluşacak ve kullanıcı NFT'sini çekemeyecektir. Üstelik ödüller muhtemelen yazarın amaçladığı şekilde bölünmemiştir. Orijinalde dört stake eden varsa ve bir kişi çekilirse, çekilme anında dizi uzunluğu 3 olduğundan ödüllerin üçte birini alacaktır.
Bazı tahminlere göre 100 milyon doların üzerinde hasara neden olan gerçek bir örnek verelim. Bileşik protokolünü tam olarak anlamadıysanız endişelenmeyin, yalnızca ilgili kısımlara odaklanacağız. (Ayrıca Bileşik protokolü, DeFi tarihindeki en önemli ve önemli protokollerden biridir; bunu DeFi eğitim kampımızda öğretiyoruz, dolayısıyla protokole ilişkin ilk izleniminiz buysa, yanlış yönlendirilmeyin).
Her neyse, Compound'un amacı, boşta kalan kripto para birimini, onu kullanabilecek diğer yatırımcılara ödünç veren kullanıcıları ödüllendirmektir. Borç verenlere hem faiz hem de COMP tokenleri cinsinden ödeme yapılır (borç alanlar COMP token ödülü talep edebilirler, ancak şu anda buna odaklanmayacağız).
Bileşik Denetleyici, çağrıları Bileşik Yönetişim tarafından belirlenebilecek uygulamalara devreden bir vekil sözleşmesidir.
30 Eylül 2021 tarihli 62 numaralı yönetişim teklifinde uygulama sözleşmesi, güvenlik açığı bulunan bir uygulama sözleşmesi olarak belirlendi. Yayına girdiği gün, Twitter'da bazı işlemlerin sıfır token stake edilmesine rağmen COMP ödülleri talep ettiği gözlemlendi.
Güvenlik açığı bulunan fonksiyon distributSupplierComp()
İşte orijinal kod
/** * @notice Calculate COMP accrued by a supplier and possibly transfer it to them * @param cToken The market in which the supplier is interacting * @param supplier The address of the supplier to distribute COMP to */ function distributeSupplierComp(address cToken, address supplier) internal { // TODO: Don't distribute supplier COMP if the user is not in the supplier market. // This check should be as gas efficient as possible as distributeSupplierComp is called in many places. // - We really don't want to call an external contract as that's quite expensive. CompMarketState storage supplyState = compSupplyState[cToken]; uint supplyIndex = supplyState.index; uint supplierIndex = compSupplierIndex[cToken][supplier]; // Update supplier's index to the current index since we are distributing accrued COMP compSupplierIndex[cToken][supplier] = supplyIndex; if (supplierIndex == 0 && supplyIndex > compInitialIndex) { // Covers the case where users supplied tokens before the market's supply state index was set. // Rewards the user with COMP accrued from the start of when supplier rewards were first // set for the market. supplierIndex = compInitialIndex; } // Calculate change in the cumulative sum of the COMP per cToken accrued Double memory deltaIndex = Double({mantissa: sub_(supplyIndex, supplierIndex)}); uint supplierTokens = CToken(cToken).balanceOf(supplier); // Calculate COMP accrued: cTokenAmount * accruedPerCToken uint supplierDelta = mul_(supplierTokens, deltaIndex); uint supplierAccrued = add_(compAccrued[supplier], supplierDelta); compAccrued[supplier] = supplierAccrued; emit DistributedSupplierComp(CToken(cToken), supplier, supplierDelta, supplyIndex); }
İronik bir şekilde hata TODO yorumunda. “Kullanıcı tedarikçi pazarında değilse tedarikçi COMP'yi dağıtmayın.” Ancak kodda bunun için bir kontrol yok. Kullanıcı staking tokenını cüzdanında tuttuğu sürece (CToken(cToken).balanceOf(supplier);), o zaman
Teklif 64, hatayı 9 Ekim 2021'de düzeltti.
Bunun bir giriş doğrulama hatası olduğu iddia edilse de kullanıcılar kötü niyetli hiçbir şey göndermedi. Birisi herhangi bir şeyi riske atmadığı için ödül talep etmeye çalışırsa doğru hesaplama sıfır olmalıdır. Muhtemelen, bu daha çok bir iş mantığı veya küçük bir hatadır.
Gerçek dünyada çoğu zaman meydana gelen DeFi saldırıları yukarıdaki hoş kategorilere girmez.
Eşlik cüzdanının doğrudan kullanılması amaçlanmamıştır. Akıllı sözleşme klonlarının işaret edeceği bir referans uygulamasıydı. Uygulama, klonların istenirse kendi kendilerini yok etmelerine izin verdi, ancak bu, tüm cüzdan sahiplerinin bunu imzalamasını gerektiriyordu.
// throw unless the contract is not yet initialized.modifier only_uninitialized { if (m_numOwners > 0) throw; _; } function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized { initDaylimit(_daylimit); initMultiowned(_owners, _required); }
Cüzdan sahipleri belli oldu
// kills the contract sending everything to `_to`.function kill(address _to) onlymanyowners(sha3(msg.data)) external { suicide(_to); }
Bazı literatür bunu “korunmasız kendi kendini yok etme”, yani erişim kontrolü hatası olarak tanımlamaktadır, ancak bu tam olarak doğru değildir. Sorun, initWallet işlevinin uygulama sözleşmesinde çağrılmamasıydı ve bu, birisinin initWallet işlevini kendisinin çağırmasına ve kendisini sahibi yapmasına izin vermesiydi. Bu onlara öldürme fonksiyonunu çağırma yetkisini verdi. Temel neden, uygulamanın başlatılmamasıydı. Bu nedenle hata, hatalı sağlamlık kodu nedeniyle değil, hatalı bir dağıtım süreci nedeniyle ortaya çıktı.
Bu hackte hiçbir Solidity kodundan yararlanılmadı. Bunun yerine, saldırganlar Cloudflare API anahtarını ele geçirdi ve web sitesinin ön ucuna, para çekme işlemlerini saldırganın adresine yönlendirecek şekilde kullanıcı işlemlerini değiştiren bir komut dosyası enjekte etti. Bu makalede daha fazlasını okuyun.
Başında çok sayıda sıfır bulunan adresleri keşfetmenin motivasyonu, bunların gaz açısından daha verimli kullanılmasıdır. Bir Ethereum işleminde, işlem verilerindeki sıfır bayt için 4 gas ve sıfır olmayan bayt için 16 gas ücretlendirilir.
Bu nedenle Wintermute, küfür adresini ( writeup ) kullandığı için saldırıya uğradı. Burada 1inch'in küfürlü adres oluşturucunun nasıl ele geçirildiğine dair yazısı var.
Güven cüzdanında bu makalede belgelenen benzer bir güvenlik açığı vardı ( https://blog.ledger.com/Funds-of-every-wallet-created-with-the-Trust-Wallet-browser-extension-could-have-been- çalıntı/ )
Akıllı sözleşmelerin özel anahtarları olmadığından bunun, create2'deki tuzun değiştirilmesiyle keşfedilen, başında sıfır bulunan akıllı sözleşmeler için geçerli olmadığını unutmayın.
Eliptik Eğri imzasındaki “r” ve “s” noktası aşağıdaki gibi oluşturulur
r = k * G (mod N) s = k^-1 * (h + r * privateKey) (mod N)
G, r, s, h ve N'nin tümü herkesçe bilinmektedir. Eğer “k” public olursa, o zaman “privateKey” bilinmeyen tek değişken olur ve çözülebilir. Bu nedenle cüzdanların mükemmel bir şekilde rastgele k üretmesi ve onu asla yeniden kullanmaması gerekir. Rastgelelik tamamen rastgele değilse, o zaman k çıkarılabilir.
Java kütüphanesindeki güvensiz rastgelelik üretimi, 2013'te pek çok Android bitcoin cüzdanını savunmasız bıraktı. (Bitcoin, Ethereum ile aynı imza algoritmasını kullanıyor.)
Bu listedeki anti-örüntüleri hızlı bir şekilde tanımak için kendinizi eğitmek sizi daha etkili bir akıllı sözleşme programcısı yapacaktır, ancak sonuçta ortaya çıkan akıllı sözleşme hatalarının çoğu, amaçlanan iş mantığı ile kodun gerçekte yaptığı şey arasındaki uyumsuzluktan kaynaklanmaktadır.
Akıllı sözleşme birim testi, akıllı sözleşme için tartışmasız en temel korumadır, ancak şok edici sayıda akıllı sözleşme ya bunlardan yoksundur ya da yetersiz test kapsamına sahiptir.
Ancak birim testleri yalnızca sözleşmelerin "mutlu yolunu" (beklenen/tasarlanan davranışı) test etme eğilimindedir. Şaşırtıcı durumları test etmek için ek test metodolojilerinin uygulanması gerekir.
Buradaki bazı metodolojilere aşina olmayanlar için, Cyfrin Audits'ten Patrick Collins, videosunda durum bilgisi olan ve durum bilgisi olmayan fuzzing'e esprili bir giriş yapıyor.
Bu görevleri gerçekleştirmek için kullanılan araçlar hızla yaygınlaşıyor ve kullanımı kolaylaşıyor.
Bazı yazarlar bu Depolarda önceki DeFi saldırılarının bir listesini derlediler:
Secureum, güvenliği incelemek ve uygulamak için yaygın olarak kullanılmaktadır, ancak deponun 2 yıldır önemli ölçüde güncellenmediğini unutmayın.
Solidity Riddles depomuzla sağlamlık açıklarından yararlanma konusunda pratik yapabilirsiniz.
DamnVulnerableDeFi her geliştiricinin pratik yapması gereken klasik bir savaş oyunudur
Capture The Ether ve Ethernaut klasiklerdir ancak bazı problemlerin gerçekçi olmayacak kadar kolay olduğunu veya modası geçmiş Solidity kavramlarını öğrettiğini unutmayın.
Bazı saygın kitle kaynaklı güvenlik firmaları, üzerinde çalışılacak geçmiş denetimlerin yararlı bir listesine sahiptir.
Eğer Solidity konusunda akıcı değilseniz Ethereum akıllı sözleşmelerini denetlemeniz mümkün olmayacaktır.
Akıllı sözleşme denetçisi olmak için sektörde tanınan bir sertifika yoktur. Herkes sağlamlık denetçisi olduğunu iddia ederek bir web sitesi ve sosyal medya profilleri oluşturabilir ve hizmet satmaya başlayabilir ve çoğu bunu yaptı. Bu nedenle dikkatli olun ve bir kişiyi işe almadan önce tavsiye alın.
Akıllı sözleşme denetçisi olmak için hataları tespit etme konusunda ortalama sağlamlık geliştiricisinden önemli ölçüde daha iyi olmanız gerekir. Bu nedenle, denetçi olmaya giden "yol haritası", sözleşme hatalarını çoğu kişiden daha iyi yakalayana kadar aylarca süren aralıksız ve kasıtlı uygulamalardan başka bir şey değildir.
Güvenlik açıklarını belirleme konusunda meslektaşlarınızdan daha iyi performans gösterme kararlılığına sahip değilseniz, kritik sorunları yüksek eğitimli ve motive suçlulardan önce fark etmeniz pek olası değildir.
Akıllı sözleşme denetimi son zamanlarda kazançlı olduğu algısı nedeniyle çalışılması arzu edilen bir alan olarak algılanmaktadır. Gerçekten de, bazı hata ödüllerinin ödemeleri 1 milyon doları aştı, ancak bu son derece nadir bir istisnadır, norm değildir.
Code4rena'nın, denetim yarışmalarında rakiplerden alınan ödemeleri gösteren halka açık bir sıralama tablosu var ve bu da bize başarı oranları hakkında bazı veriler sağlıyor.
Yönetim kurulunda henüz 1171 isim var
Şunu da düşünün, Openzeppelin bir güvenlik araştırması bursu (iş değil, iş öncesi tarama ve eğitim) için başvuru açtığında, yalnızca 10'dan az adayı seçmek için 300'den fazla başvuru aldı; bunlardan çok daha azı tam burs alacaktı. zaman işi.
Bu Harvard'dan daha düşük bir kabul oranı.
Akıllı sözleşme denetimi rekabetçi, sıfır toplamlı bir oyundur. Denetlenecek çok fazla proje, güvenlik için çok fazla bütçe ve bulunacak çok fazla hata var. Güvenliği şimdi incelemeye başlarsanız, sizden büyük bir avantaj sağlayan yüksek motivasyona sahip düzinelerce kişi ve ekip vardır. Çoğu proje, denenmemiş yeni bir denetçi yerine itibar sahibi bir denetçi için prim ödemeye hazırdır.
Bu yazıda en az 20 farklı güvenlik açığı kategorisini listeledik. Her biri üzerinde uzmanlaşmak için bir hafta harcadıysanız (ki bu biraz iyimser bir durum), deneyimli denetçilerin ortak bilgisinin ne olduğunu henüz yeni anlamaya başlıyorsunuz. Bu makalede, her ikisi de bir denetçinin anlaması gereken önemli konular olan gaz optimizasyonunu veya tokenomiyi ele almadık. Matematiği yaptığınızda bunun kısa bir yolculuk olmadığını göreceksiniz.
Bununla birlikte, topluluk yeni gelenlere karşı genellikle dost canlısı ve yardımseverdir ve bol miktarda ipucu ve püf noktası vardır. Ancak bu makaleyi akıllı sözleşme güvenliğinden kariyer yapma umuduyla okuyanlar için, kazançlı bir kariyer elde etme ihtimalinin sizin lehinize olmadığını açıkça anlamak önemlidir. Başarı varsayılan sonuç değildir.
Elbette yapılabilir ve pek çok kişi Sağlamlık bilmemekten denetim alanında kazançlı bir kariyere sahip olmaya geçti. İki yıllık bir süre içinde akıllı sözleşme denetçisi olarak iş bulmak, hukuk fakültesine kabul edilip baro sınavını geçmekten muhtemelen daha kolaydır. Diğer birçok kariyer tercihiyle karşılaştırıldığında kesinlikle daha avantajlıdır.
Ancak yine de önünüzde hızla gelişen bilgi dağında ustalaşmak ve böcekleri tespit etmek için sezgilerinizi geliştirmek sizin açınızdan muazzam bir azim gerektirecektir.
Bu, akıllı sözleşme güvenliğini öğrenmenin değerli bir uğraş olmadığı anlamına gelmez. Kesinlikle öyle. Ancak sahaya gözünüzde dolar işaretiyle yaklaşıyorsanız beklentilerinizi kontrol altında tutun.
Bilinen anti-kalıpların farkında olmak önemlidir. Ancak gerçek dünyadaki hataların çoğu uygulamaya özeldir. Her iki güvenlik açığı kategorisinin belirlenmesi sürekli ve kasıtlı uygulama gerektirir.
Sektör lideri sağlamlık eğitimimizle akıllı sözleşme güvenliğini ve daha birçok Ethereum geliştirme konusunu öğrenin.
Bu makalenin ana görseli HackerNoon'unYapay Zeka Görüntü Oluşturucusu tarafından "bilgisayarı koruyan bir robot" istemi aracılığıyla oluşturuldu.