Bilir misiniz, hackathon gecelerinde sabahlarken... yani şey, hangi geceler, sunucu raflarının altında günde üç saat uyuyarak geçirdiğimiz o günlerde, akıllı sözleşmelerdeki temel açıkları herhangi bir statik analizörün rahatça kapatabileceğini düşünürdüm. Ne kadar zor olabilirdi ki? Reentrancy (yeniden giriş atağı) dediğin şey işin klasikleşmiş hallerindendir, hakkında yazmayan kalmadı. Checks-Effects-Interactions deseni, türlü türlü modifier'lar falan... Derken birkaç ay önce ekibe yeni katılan bir junior, ortalıkta gururla dolanıp şöyle dedi: «Bana ChatGPT kusursuz bir staking kontratı üretti, kontrol ettim, gayet güvenli!»
Neredeyse kahveyi mekanik klavyenin üstüne döküyordum.
Şimdi aramızda kalsın: Modern LLM'ler gayet şık Solidity kodları yazıyor. Söz dizimi pırıl pırıl, import'lar düzenli, OpenZeppelin en son moda trendine göre entegre edilmiş. İş hassas durum yönetimi (state distribution) mantığına gelince ise yapay zeka; sarsılmaz, kör bir özgüvenle öyle açıklar üretmeye başlıyor ki, denetimlerde (audit) milyonlarca doların buharlaşmasına yol açıyor. Gelin ChatGPT'nin, kodunuzun CertiK denetimini rahatça geçeceğine sizi ikna etmeye çalışırken fena çuvalladığı üç sinsi duruma yakından bakalım.
Vaka #1: Blok Başına Ödüllü ve Gizli Harici Çağrılı Asenkron Staking
Düşünsenize, yapay zekadan havuzda geçirilen süreye göre token dağıtan bir sözleşme yazmasını istiyorsunuz. Model ne yapıyor? Kullanıcıya fon transferi gördüğü her yere gönül rahatlığıyla nonReentrant desenini yapıştırıyor. Fakat mimari açıdan çok kritik bir detayı atlıyor.
O junior'ın yaptığı hatadan sonra bizzat çektiğim şu gerçek kod incelemesi parçasına bir bakın:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IERC20Mintable {
function mint(address to, uint256 amount) external;
}
contract SecureStakingVault {
IERC20Mintable public rewardToken;
mapping(address => uint256) public balances;
mapping(address => uint256) public lastUpdateBlock;
uint256 public rewardRatePerBlock = 10 * 10**18;
bool private locked;
// Koruma altına aldık işte! Daha ne istiyorsun, paranoyak herif?
modifier noReentry() {
require(!locked, "LOCKED");
locked = true;
_;
locked = false;
}
constructor(address _token) {
rewardToken = IERC20Mintable(_token);
}
function stake() external payable {
require(msg.value > 0, "Zero stake");
balances[msg.sender] += msg.value;
lastUpdateBlock[msg.sender] = block.number;
}
// ChatGPT'nin güvenli olduğunu iddia ettiği o şaheser fonksiyon
function harvestAndUnstake(uint256 _amount) external noReentry {
require(balances[msg.sender] >= _amount, "Insufficient balance");
uint256 blocksPassed = block.number - lastUpdateBlock[msg.sender];
uint256 reward = blocksPassed * rewardRatePerBlock * _amount / balances[msg.sender];
// Dikkat: State güncellemesi harici mint çağrısından ÖNCE yapılıyor
balances[msg.sender] -= _amount;
lastUpdateBlock[msg.sender] = block.number;
// Ödül basımı bir token constructor'ını veya hook'u tetikler, ve işte... sürpriz!
rewardToken.mint(msg.sender, reward);
(bool success, ) = msg.sender.call{value: _amount}("");
require(success, "Transfer failed");
}
}Buradaki bit yeniği nerede? Model, bakiye değişkenleri rewardToken.mint() öncesinde sıfırlandığı için saldırının imkansız olduğuna körü körüne inanıyor. Ancak rewardToken, mint sözleşmesinde _beforeTokenTransfer hook'u veya standart bir ERC-777 benzeri callback barındıran özel (custom) bir ERC-20 token'ıdır (eğer token karmaşık veya proxy tabanlıysa). mint çağrıldığı anda kontrol, ETH kullanıcıya ulaşmadan hemen önce harici koda geçer; fakat aktif oturum sırasında havuzun durumu çoktan tek taraflı değiştirilmiş ve diğer kullanıcılar için ödül hesaplamaları altüst olmuştur. Standart ders kitaplarının sessiz kaldığı yeni nesil birler arası (cross-contract) Reentrancy vakası tam olarak budur.
Vaka #2: Likidite Kehanetleri (Oracles) Üzerinden Çapraz Fonksiyonlu Sızma
Modern exploit'lerin sinsi yönü, reentrancy'nin çoğu zaman tek bir fonksiyonun içinde değil, ortak bir mapping'i paylaşan tamamen farklı iki giriş noktası (entry point) arasında yer almasıdır. Yapay zeka, mantığı "temiz" modüllere bölmeyi çok sever, ancak global bir değişmezin (invariant) güncellenme sırasını tamamen göz ardı eder.
- Açıklık Parametresi: Statik analizör değerlendirmesi
- Gerçek Durum: Modifier'lar
- Mevcut Olanlar (nonReentrant): Çapraz fonksiyon çağrılarına karşı tamamen işlevsiz
- İşlem Sırası: Checks-Effects yerel düzeyde gözetilmiş
- Bozulan Global Protokol Dengesi: ChatGPT'nin tepkisi
- «Kod yeniden girişe karşı tamamen korumalıdır»: Çapraz durum manipülasyonuna davetiye çıkarır
Vaka #3: Akışkan Süreli Sözleşmeler ve ERC-4626 Kasaları
ChatGPT, getiri kasaları (ERC-4626) için OpenZeppelin'in standart şablonlarına adeta kafayı takmış durumdadır. Hazır örnekleri alır, pay hesaplaması için kendi uydurma matematiğini ekler ve ilk denemede derlenen bir kod ortaya çıkarır. Fakat hisseleri varlıklara dönüştüren fonksiyon (convertToAssets), ara bir sözleşme üzerinden belirli koşullarla çağrıldığında, sanal likidite hesaplaması esnasında çok katmanlı bir kontrol iadesi (control flow return) tetiklenmesine olanak tanır.
Üç hafta önce testnet üzerinde bir bug ayıklamak için gece yarısına kadar kendi başıma uğraşıyordum; simülasyon botu tam olarak bu şemayı kullanarak havuzdaki tüm likiditeyi çekip süpürmüştü. Sinir bozucu olan şu ki, yapay zeka bana «Hassasiyet için çarpan ölçeklendirmesi kullanın» önerisinde bulunuyordu. Sonuçta elde ettiğimiz şey ise, harici bir sistemin kasa durumunu henüz kesinleşmemiş ara aşamada okumasıyla oluşan klasik bir read-only reentrancy açığıydı.
Dil modellerinin bize "basit ve güvenilir bir akıllı sözleşme" yazmalarını istediğimizde önümüze koydukları bu cehennemî saçmalık tiyatrosuna dalmaya devam ediyoruz.
Eski bir güvenlik denetçisi olarak çalışmanın en eğlenceli yanını biliyor musunuz? Geliştiricilerin ChatGPT'nin kendi yorumlarına nasıl körü körüne güvendiğini izlemek. Yapay zeka üst tarafa // Safe against reentrancy attacks thanks to Checks-Effects-Interactions gibi bir satır atıyor — ve tamamdır, adamın eleştirel düşünme becerisi anında devre dışı kalıyor. Gözler kör oluyor, beyin dinlenmeye çekiliyor ve sonra mainnet'te muazzam bir havai fişek gösterisi kopuyor.
Yapay zekaların ısrarla güvenlik standartı olarak gördüğü, ancak pratikte çatırdaya çatırdaya patlayan iki güçlü örneği daha masaya yatıralım.
Vaka No. 4: Flash Loan Geri Ödemeli Çok Adımlı Arbitraj
Hackathon'larda sıkça verilen veya junior dev'lere yıkılan klasik bir görev: Flash crypto çeken, bunu DEX üzerinden döndüren, karı toplayıp ana borcu ödeyen ve komisyonu likidite havuzuna sabitleyen bir sözleşme yazmak. ChatGPT ne yapıyor? İşlemin başında ve sonunda bakiye kontrolleri için nazikçe require koşulları yerleştiriyor; delta eşleşiyorsa sistemin zapt edilemez olduğuna saf bir şekilde inanıyor.
Fakat şeytan, her zamanki gibi uniswapV2Call callback fonksiyonunun (veya diğer protokollerin benzer arayüzlerinin) çağrı detaylarında gizlidir.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IUniswapV2Pair {
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}
contract AIArbitrageBotHolder {
address private immutable owner;
bool private unlocked = true;
// Yapay zeka, tüm mantığı korumak için bu bariyerin yeterli olduğunu düşünür
modifier lock() {
require(unlocked, "LOCKED");
unlocked = false;
_;
unlocked = true;
}
constructor() {
owner = msg.sender;
}
// Yapay zekaya göre o meşhur "güvenli" metod
function executeFlashLoan(address _pair, uint256 _amountA, bytes calldata _data) external lock {
// Havuzdan likidite talebi
IUniswapV2Pair(_pair).swap(_amountA, 0, address(this), _data);
}
// Havuzun kontrolü geri verdiği Callback fonksiyonu
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
// Acımasız hata: Kodun "estetik" durması için msg.sender kontrolü yapay zeka tarafından sıkça es geçilir
// Voila, herhangi bir dış sözleşme buraya gelip bir reentrancy döngüsü tetikleyebilir
uint256 fee = amount0 * 3 / 997 + 1;
uint256 repayment = amount0 + fee;
// ChatGPT tarafından yazılan özel arbitraj mantığı
_executeInternalArbitrage(amount0);
// Borcun havuza geri ödenmesi
// (Burada dış havuzların state'inin henüz kesinleşmediği unutulur)
bool success = IERC20(msg.sender).transfer(msg.sender, repayment);
require(success, "Repayment failed");
}
function _internalArbitrage(uint256 amount) internal {
// Karmaşık takas mantığı için taslak fonksiyon
}
}Açığı gördünüz mü? Yapay zeka lock modifikatörünü dış giriş noktası olan executeFlashLoan üzerine koyuyor, ancak uniswapV2Call geri çağrı fonksiyonunun ayrı olarak izole edilmesi gerektiğini veya çağıran adresi (msg.sender) katı bir şekilde doğrulaması icap ettiğini tamamen unutuveriyor. Bir saldırgan, ara hesaplamaların yürütüldüğü ve sarmalayıcı sözleşmenin bakiyelerinin henüz tutarlı bir duruma getirilmediği anda bu callback'i doğrudan kendi sözleşmesinden çağırıp teminatı son kuruşuna kadar boşaltabilir. ChatGPT size bunu asla söylemez; sohbet penceresinde sadece o sevimli emojileriyle sırıtıp şöyle der: "Kod, ERC standardına göre optimize edilmiştir."
Vaka No. 5: Önbelleğe Alınmış Optimize Edilmiş ERC-721 Toplu Mint Sözleşmeleri
Modern NFT piyasası ucuz gas ücretleri ister. Yapay zeka bunu kusursuz bir şekilde anlar ve sözleşmenin belleğinde doğrudan özel sahip eşleme yapıları (mapping) ile önbellek sayaçları uygulayarak kodu "optimize" etmeye koyulur.
- Denetim Metriği: ChatGPT'nin Kararı — Gerçek Zafiyet Durumu
- Gas Tüketimi: Ultra Düşük (Gas Optimized) — State izolasyonu ihlal edilerek elde edilir
- Depolama Deseni: Bellekte yerel yapılar (struct) kullanımı — onERC721Received hook'ları aracılığıyla manipülasyona açık
- Kararlılık: Standart Hardhat testlerini geçer — Derin reentrancy çağrı yığınlarında (stack) çöker
Sözleşme safeMint üzerinden NFT gönderdiğinde, standart gereği alıcı akıllı sözleşmedeki onay fonksiyonunun çağrılması gerekir. Eğer toplu mint yapıyorsanız (diyelim ki tek seferde 10 token) ve her token gönderiminden sonra (veya harici bir çağrı içindeki döngüde) toplam bakiye sayacını güncelliyorsanız, saldırgan üçüncü iterasyonda kontrolü ele geçirir; bu sırada dahili token dizisi henüz yarıya kadar dolmuştur ancak işlem sayacı sürecin daha yeni başladığını sanmaktadır. Sonuç olarak indeks çakışmaları ve yoktan var edilen sonsuz varlıklar elde ederiz.
Ben, siz ve tüm sektör buna karşı ne yapmalıyız?
Bakın, ben de her gün kod yazıyorum, ancak kripte üretken modellere körü körüne güvenmek, tekerleğinde tek bir boşluk hariç tüm mermiler dolu olan bir Rus ruleti oynamak gibidir. Protokol üretmek için yapay zeka kullanıyorsanız şu üç sert kuralı aklınızdan çıkarmayın:
- Şablonlardan gelen modifikatörlere asla inanmayın. Yapay zeka araya
nonReentrantsıkıştırdıysa, bu sadece o kelimeyi bildiği anlamına gelir; spesifik blok zincirinizin eşzamansız (asynchronous) bağlamını anladığı anlamına gelmez. - Callback fonksiyonlarında her zaman
msg.senderkontrolü yapın.uniswapV2Call,onERC721Received,tokensReceivedgibi her türlü arayüz askeri bir sığınak kapısından çok daha sıkı korunmalıdır. - Foundry üzerinde kendi fuzz testlerinizi yazın. Hiçbir statik birim testi (unit test), birkaç milyon rastgele invariant (değişmezlik) çalıştırması kadar çapraz fonksiyonlu reentrancy açıklarını bulamaz.
Bu güvenlik açıkları hakkında zihnimi yırtarcasına konuşup dururken, kafamı kurcalayan o tek düşünce bir türlü yakamı bırakmıyor: Aslında piyasayı tembelleştiren bizzat kendimiziz. Rutin görevleri kod üreten araçlara devretmeye o kadar alıştık ki, blockchain'in mantık derleme hatalarını asla affetmediğini unuttuk. Geleneksel web geliştirmede bir commit'i geri alabilir, patch atabilir, beş dakikalık kesinti için kullanıcılardan özür dileyebilirsiniz. Ancak EVM ortamında hatanız, insan dikkatsizliğinin ve algoritmik körlüğün zaferini temsil eden bir anıt gibi, değiştirilemez bir ledger'da sonsuza kadar kalır.
Konunun geri kalanını da aradan çıkaralım ve ChatGPT'nin korkutucu bir tutarlılıkla ürettiği o bariz olmayan vektörlerden birini daha inceleyelim.
Vaka #6: Likidite Agregatörleri ve Stablecoin Havuzlarındaki Sanal Paylar
Bu, DeFi geliştiricilerinin son gözdesi—constant-product eğrilerine dayalı dinamik token ağırlık hesaplamalarına sahip özel takas havuzları oluşturmak. Yapay zeka modelleri bu matematiğe bayılıyor çünkü internette Curve ve Uniswap v2'nin terabaytlarca açık kaynaklı kodu dolaşıyor. Model formülü alıyor, ihtiyaçlarınıza göre uyarlıyor, işin içine bir addLiquidity fonksiyonu sıkıştırıyor ve ürünün canlıya alınmaya hazır olduğunu gururla rapor ediyor.
Tek sorun şu ki modelin; değişken komisyonlu (USDT gibi veya transfer vergisine sahip özel deflationary tokenler gibi) harici tokenler çağrıldığında sanal bakiyelerin asenkron hesaplamasının nasıl çalıştığı hakkında absolutamente hiçbir fikri yok.
Yapay zekanın tamamen kusursuz olduğunu düşündüğü şu desene bir bakın:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface ITokenWithFee {
function transferFrom(address sender, address recipient, amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract AIWarpPool {
mapping(address => uint256) public userShares;
uint256 public totalShares;
// Yapay zeka düşünüyor ki önce bakiyeyi hesaplayıp sonra mapping'e yazarsak güvendeyiz
function deposit(address token, uint256 amount) external {
uint256 balanceBefore = ITokenWithFee(token).balanceOf(address(this));
// Harici token transfer çağrısı
bool success = ITokenWithFee(token).transferFrom(msg.sender, address(this), amount);
require(success, "Transfer failed");
uint256 balanceAfter = ITokenWithFee(token).balanceOf(address(this));
uint256 realReceived = balanceAfter - balanceBefore;
// Fiilen gelen fonlar üzerinden pay (share) basımı
uint256 shares = totalShares == 0 ? realReceived : (realReceived * totalShares) / balanceBefore;
userShares[msg.sender] += shares;
totalShares += shares;
}
function withdraw(uint256 shares) external {
require(userShares[msg.sender] >= shares, "Not enough shares");
uint256 amountToReturn = (shares * ITokenWithFee(msg.sender).balanceOf(address(this))) / totalShares;
// Önce state'i güncelliyoruz...
userShares[msg.sender] -= shares;
totalShares -= shares;
// ...ve ardından fonları gönderiyoruz. Klasik Checks-Effects-Interactions!
// Ama bir dakika ya...
(bool success, ) = msg.sender.call{value: 0}(""); // Koşullu harici token transfer çağrısı
require(success, "Withdraw failed");
}
}Tuzağı gördünüz mü? Yapay zeka, withdraw fonksiyonu içinde Checks-Effects-Interactions kuralına namuslu bir şekilde uymuş. Ancak amountToReturn hesaplamasının, çağrı anındaki güncel balanceOf(address(this)) değerine bağlı olduğunu tamamen gözden kaçırmış. Eğer kötü niyetli bir aktör, kontrolü ele geçirdiğinde (veya token callback'i aracılığıyla) küresel totalShares başka bir bağlı havuzda değişmeden ya da çapraz sözleşme (cross-contract) oracle çağrısı gerçekleşmeden önce withdraw fonksiyonuna tekrar giren (reentrancy) bir sarmalayıcı (wrapper) sözleşme oluşturursa, oranı eskimiş likidite değeri üzerinden hesaplatmış olur.
Yapay Zekanın Sizin İçin Yazdığı Kodları Denetlemek İçin Final Kontrol Listesi
Dağıtık sistemler mimarisinde yapay zekanın sizden daha akıllı olduğunu umarak aynı duvara çarparak durmayı bırakın. Ürettiğiniz kodu prod ortamına veya gerçek paranın olduğu bir testnet'e göndermeden önce şu listeyi gözden geçirin:
- Tüm callback fonksiyonlarını, içlerinde hazır bir exploit'le oturan bir hacker varmış gibi izole edin.
- Her harici çağrıyı (external call), küresel invariant'lar (değişmezler) henüz tam oturmamışken sözleşmeye kontrolü geri verip veremeyeceği açısından kontrol edin.
- Modifier'lara körü körüne güvenmeyi unutun — onlar yalnızca aynı fonksiyona yapılan doğrudan yeniden girişlere (reentrancy) karşı koruma sağlar, ancak karmaşık çapraz sözleşme zincirleri karşısında tamamen çaresizdirler.