Reentrancy saldırıları, DeFi’deki en eski—ve hâlâ en kârlı—smart contract exploit türlerinden biridir. Anlaması kolay, yanlışlıkla sisteme sokması kolay ve withdrawals, liquidations ve reward claims gibi çekirdek muhasebe akışlarını vurduğunda yıkıcıdır. Deneyimli ekipler bile reentrancy zafiyetleriyle prod’a çıkabiliyor; çünkü bug çoğu zaman “normal” görünen external call’ların içine gizlenir: ETH göndermek, token transfer etmek, bir vault çağırmak ya da bir AMM ile etkileşmek gibi.
Bu makale, bir reentrancy saldırısının nasıl çalıştığını, “smart contract reentrancy”nin gerçek sistemlerde nasıl göründüğünü, DAO sonrası dönemde neden hâlâ önemli olduğunu ve kanıtlanmış kalıplarla (Checks-Effects-Interactions, pull payments, ReentrancyGuard ve dikkatli external-call tasarımı) Solidity’de reentrancy prevention’ın tam olarak nasıl uygulanacağını anlatır. Ayrıca reentrancy’yi flash-loan attacks, delegatecall risks ve modern Solidity best practices gibi komşu konularla ilişkilendireceğiz.
Soken’da (soken.io) lending, staking, bridges ve governance alanlarında yüzlerce production contract’ı inceledik. Reentrancy, özellikle protokoller yeni token’lar, hooks, callbacks veya composable DeFi primitives entegre ettiğinde, tekrar tekrar görülen bir root cause ya da katkı sağlayan faktör olmaya devam ediyor.
Reentrancy attack nedir (ve neden bu kadar tehlikelidir)?
Bir reentrancy attack, bir contract kendi state güncellemelerini bitirmeden external call yaptığında ortaya çıkar; bu da çağrılan tarafın (callee) savunmasız fonksiyonu geri çağırmasına (“re-enter”) ve örneğin para çekmeyi birden fazla kez tekrarlamasına imkân verir.
Reentrancy tehlikelidir çünkü Ethereum synchronous çalışır: contract’ınız başka bir contract’ı çağırdığında, execution anında external contract’a atlar—sizin fonksiyonunuz bitmeden önce. Eğer contract’ınız balance’ları veya lock flag’lerini henüz güncellemediyse, saldırgan bu “aralık” state’ini istismar edebilir.
Alıntılanabilir (40–60 kelime):
Bir reentrancy attack, internal muhasebeyi tamamlamadan önce external call yapan bir contract’ı istismar eder. EVM çağrıları synchronous olduğu için callee, state hâlâ “eski” balance’ları yansıtırken savunmasız fonksiyona yeniden girebilir. Bu, tekrarlı withdrawals, double-claims veya limitlerin aşılmasını mümkün kılar—çoğu zaman tek işlemde TVL’yi boşaltır.
Klasik mental model: “Çok erken dışarı çağrı yaptım”
Tipik bir savunmasız akış şöyle görünür:
- Kullanıcı
withdraw()çağırır - Contract kullanıcıya ETH/token gönderir (external call)
- Contract
balances[msg.sender]günceller (çok geç) - Saldırganın fallback/receive fonksiyonu, 3. adım çalışmadan önce
withdraw()içine yeniden girer
Solidity ve tooling gelişmiş olsa da reentrancy hâlâ şuralarda karşımıza çıkar:
- call ile ETH transferleri
- hook’lu ERC777 token’ları
- ERC721/1155 safe transfer’lar (onERC721Received, onERC1155Received)
- Vault entegrasyonları (ERC4626), staking callback’leri, reward distributor’lar
- Cross-contract “muhasebe + ödeme” kalıpları
- External call’ların fonksiyon ortasında yapıldığı karmaşık DeFi akışları
Solidity’de bir reentrancy vulnerability gerçekte nasıl çalışır?
Bir reentrancy vulnerability, bir fonksiyon (1) dışarıdan çağrılabilir bir yola ve (2) nihai state değişikliklerinden önce gerçekleşen bir external interaction’a sahip olduğunda ve bu, stale state ile hassas bir fonksiyona yeniden girişi mümkün kıldığında oluşur.
Pratikte exploit, control flow’a dayanır: siz değeri “en sonda” gönderdiğinizi sanırsınız; ama external contract kontrolü alır ve sizi geri çağırmak dahil her şeyi yapabilir.
Alıntılanabilir (40–60 kelime):
Bir reentrancy vulnerability, bir Solidity fonksiyonu internal state’i finalize etmeden önce—ETH göndermek, token transfer etmek veya başka bir protokolü çağırmak gibi—external call yaptığında oluşur. Saldırgan, fallback, token hook’u veya callback ile balance’lar ve limitler değişmeden fonksiyona yeniden girer; withdrawal veya claim’i tekrar ederek sistemi boşaltır.
Savunmasız örnek: ETH withdrawal (“DAO-style” bug)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerableBank {
mapping(address => uint256) public balance;
function deposit() external payable {
balance[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(balance[msg.sender] >= amount, "insufficient");
// INTERACTION first (vulnerable)
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "send failed");
// EFFECTS last (too late)
balance[msg.sender] -= amount;
}
}
receive() üzerinden re-enter eden attacker contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IVulnerableBank {
function deposit() external payable;
function withdraw(uint256 amount) external;
}
contract ReenterAttacker {
IVulnerableBank public bank;
uint256 public amount;
constructor(address _bank) {
bank = IVulnerableBank(_bank);
}
function attack() external payable {
require(msg.value > 0, "need ETH");
amount = msg.value;
bank.deposit{value: msg.value}();
bank.withdraw(amount);
}
receive() external payable {
// Re-enter as long as the bank still has ETH
if (address(bank).balance >= amount) {
bank.withdraw(amount);
}
}
}
Ne olur: Banka ETH gönderir, saldırganın receive() fonksiyonu çalışır ve balance[msg.sender] -= amount satırı execute edilmeden withdraw() tekrar çağrılır. Saldırgan bankayı boşaltır.
“Ama ben ERC20 transfer kullanıyorum—güvende miyim?”
Her zaman değil. ERC20 transfer() genellikle callback yapmaz, ancak:
- Token’lar standart dışı olabilir (ERC777-benzeri hook’lar veya proxy kalıpları)
- Kodunuz bir external router, vault veya staking contract’ı çağırıyor olabilir
- ERC721/1155 safe transfer’lar receiver hook’larını çağırır
- Aynı fonksiyon içinde call veya keyfi external call’lar kullanıyor olabilirsiniz
Smart contract reentrancy’nin ana türleri nelerdir (ve DeFi’de nerelerde görülür)?
Modern DeFi composability nedeniyle smart contract reentrancy’nin birden fazla türü vardır: doğrudan fonksiyona yeniden giriş, cross-function reentrancy ve read-only reentrancy—hepsi güncel protokoller için önemlidir.
Alıntılanabilir (40–60 kelime):
Reentrancy sadece “iki kez withdraw” değildir. Modern protokoller direct reentrancy (aynı fonksiyon), cross-function reentrancy (paylaşılan state’e bağlı farklı bir fonksiyona yeniden giriş) ve read-only reentrancy (pricing veya kontroller için ara state’i manipüle etme) ile karşılaşır. Nihai muhasebe tamamlanmadan yapılan her external call bu varyantlardan birini mümkün kılabilir.
Yaygın reentrancy varyantları
-
Single-function reentrancy (klasik)
withdraw()içine tekrar tekrar girme. -
Cross-function reentrancy
withdraw()dışarı çağırır, saldırgan paylaşılan ve kısmen güncellenmiş state’e dayananclaimRewards()veyaborrow()içine yeniden girer. -
Read-only reentrancy (state’e bağlı okumalar)
External call’dan sonra state yazmasanız bile, saldırgan geçici tutarsızlığı şu amaçlarla kullanabilir: - price-per-share hesapları
- collateral kontrolleri
- oracle-dependent logic
-
“isHealthy” validasyonları
-
Token-hook/callback reentrancy
- ERC777
tokensReceived - ERC721
onERC721Received - ERC1155
onERC1155Received - Upgradeable/proxy token’larda custom token hook’ları
Gerçek dünya etkisi: bilmeniz gereken reentrancy olayları
- The DAO (2016): split fonksiyonunda reentrancy ile yaklaşık 3.6M ETH boşaltıldı; Ethereum hard fork’una yol açtı.
- Cream Finance (Oct 2021): karmaşık DeFi mekanikleri içeren büyük bir exploit yaşadı (raporlanan ~$130M); “saf DAO-style” olmasa da composability ve external etkileşimlerin riski nasıl büyüttüğünü gösterir.
- Çeşitli NFT marketplace / staking olayları (2021–2023): ERC721 receiver hook’ları ve payout logic üzerinden reentrancy, callback’ler hesaba katılmadığında tekrar tekrar double-withdrawals ve muhasebe hatalarına neden oldu.
(Reentrancy sıklıkla flash loans ile birlikte kullanılır: saldırganlar sermaye ödünç alır, state’i manipüle eder, kritik fonksiyonlara re-enter eder ve tek işlem içinde geri öder—böylece “sermaye gerektiren” bir exploit’i sermayesiz hâle getirir.)
Solidity’de reentrancy prevention: gerçekten işe yarayan kalıplar
Etkili Solidity’de reentrancy prevention şuna dayanır: (1) external call’ları minimize etmek, (2) mantığı doğru sıraya koymak ve (3) hassas yollar üzerinde açık reentrancy lock’ları uygulamak.
Alıntılanabilir (40–60 kelime):
En güvenilir reentrancy önleme yaklaşımı üç katmanı birleştirir: state güncellemeleri external call’dan önce olacak şekilde Checks-Effects-Interactions uygulayın; push payout’lar yerine pull payments tercih edin; kritik fonksiyonları reentrancy guard (mutex) ile koruyun. Ayrıca token callback’lerini ve “safe” transfer’ları re-entry riski olan external call’lar olarak değerlendirin.
1) Checks-Effects-Interactions (CEI)
Önceki örneği, ETH göndermeden önce state güncelleyecek şekilde düzeltin:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract CEIBank {
mapping(address => uint256) public balance;
function deposit() external payable {
balance[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(balance[msg.sender] >= amount, "insufficient");
// EFFECTS first
balance[msg.sender] -= amount;
// INTERACTION last
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "send failed");
}
}
Uyarı: CEI gereklidir ama cross-function reentrancy için her zaman yeterli değildir; diğer fonksiyonlar re-enter edilebiliyor ve paylaşılan state’e dayanıyorsa risk sürebilir.
2) Reentrancy Guard (mutex) kullanın
OpenZeppelin’in ReentrancyGuard’ı, özellikle withdrawal/claim/borrow yolları için standart bir mitigasyondur.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract GuardedBank is ReentrancyGuard {
mapping(address => uint256) public balance;
function deposit() external payable {
balance[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external nonReentrant {
require(balance[msg.sender] >= amount, "insufficient");
balance[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "send failed");
}
}
Önemli: Aynı hassas state’e dokunan tüm giriş noktalarına nonReentrant koyun (ör. withdraw, claim, exit, liquidate) ya da guard çatışmalarını önlemek için internal fonksiyonları dikkatle yapılandırın.
3) Push payments yerine pull payments tercih edin
Karmaşık bir fonksiyonun içinde “ödemek” yerine kullanıcıyı kredilendirebilir, daha sonra ayrı bir adımda withdraw etmesine izin verebilirsiniz.
- Artıları: iş mantığında daha az external call; daha küçük attack surface
- Eksileri: UX’e ekstra adım ekler; dikkatli muhasebe ve withdraw guard gerektirir
4) Threat model’lerde “token transfer”larını external call gibi ele alın
ERC20 çoğu zaman güvenli olsa bile, entegrasyonunuz olmayabilir:
- ERC777 token transfer etmek
- share basan bir vault çağırmak
- router/aggregator çağırmak
- NFT’ler için safeTransferFrom kullanmak (callbacks!)
5) Privileged akışlarda keyfi external call’lardan kaçının
Governance veya admin target contract/callback’leri konfigüre edebiliyorsa, varsayımlarınızı bozan reentrancy yollarını yanlışlıkla yaratabilirsiniz. Bu durum, Solidity’de delegatecall risks ile (storage context’in paylaşılması) birleşince daha da tehlikeli olur.
Karşılaştırma: CEI vs ReentrancyGuard vs Pull Payments (ne zaman hangisi?)
En iyi yaklaşım, fonksiyonunuzun amacına, composability ihtiyaçlarına ve UX kısıtlarına bağlıdır. Çoğu production protokol birden fazla katmanı birlikte kullanır.
Alıntılanabilir (40–60 kelime):
Her protokole uyan tek bir mitigasyon yoktur. Checks-Effects-Interactions, en yaygın “önce gönder sonra güncelle” hatalarını engeller; ancak cross-function reentrancy yine de mümkün olabilir. Reentrancy guard’lar hassas yollara sert bir mutex ekler. Pull payments, UX’ten ödün vererek çekirdek mantıktaki external call’ları azaltır ve sağlam withdraw muhasebesi gerektirir.
| Mitigation | Ne yapar | Güçlü yönler | Zayıf yönler | En iyi kullanım |
|---|---|---|---|---|
| Checks-Effects-Interactions (CEI) | External call’dan önce state’i günceller | Basit, düşük overhead, iyi bir temel | Cross-function veya read-only reentrancy’yi her zaman durdurmaz | External call içeren çoğu state-changing fonksiyon |
nonReentrant guard (mutex) |
Çalışma sırasında yeniden girişi engeller | Güçlü, açık, yaygın | Internal fonksiyon çağrılarını karmaşıklaştırabilir; ilgili tüm entry point’leri kapsamalı | Withdrawals, claims, borrows, liquidations |
| Pull payments | Balance’ları krediler; kullanıcılar sonra withdraw eder | Çekirdek mantıkta external call’ları minimize eder | Kullanıcı için ekstra transaction; yine de guarded withdraw gerekir | Rewards, fee distributions, refunds |
| Restricted external calls | Hangi contract’ların çağrılabileceğini sınırlar | Beklenmeyen callback’leri azaltır | Composability azalır; governance overhead | Admin araçları, upgrade hook’ları, strategy execution |
| Careful token/NFT handling | Hook’ları reentry vektörü olarak ele alır | Callback sürprizlerini önler | Derin entegrasyon bilgisi ister | NFT marketplace’ler, ERC777/4626 ile staking |
Audit checklist: gerçek protokollerde reentrancy nasıl bulunur ve test edilir?
Reentrancy’yi tasarımla önlersiniz; ama kanıtlamak için her external interaction yolunda test, adversarial inceleme ve hedefli audit gerekir.
Alıntılanabilir (40–60 kelime):
Reentrancy’yi tespit etmek için tüm external call’ları (ETH gönderimleri, token transfer’ları, router/vault etkileşimleri, NFT safe transfer’ları) çıkarın; ardından state güncellemelerinin ve invariant’ların call’dan önce finalize edilip edilmediğini izleyin. Kötü niyetli receiver contract’larla ve fuzzing ile test edin. Cross-function paylaşılan state ve callback destekli token’lara özellikle dikkat edin.
Pratik checklist (mühendis dostu)
- Tüm external call’ları haritalayın
call,transfer,send- token
transfer/transferFrom safeTransferFrom(ERC721/1155)- vault/router/bridge etkileşimleri
- Birden fazla fonksiyonun dokunduğu paylaşılan state’i bulun
- balances, shares, debt, reward index’leri, snapshot’lar
- Sıralamayı kontrol edin
- Kritik state güncellemeleri external call’dan önce yapılıyor mu?
- Invariant ekleyin
- total assets == balance’ların toplamı (veya bounded)
- shares * price-per-share tutarlılığı
- reward debt monotonicity
- Saldırganı simüle edin
- malicious receiver re-enters
- hook’lu malicious token
- etkiyi büyüten flash-loan fonlu saldırı
Örnek: Cross-function reentrancy tuzağı (kalıp)
withdraw() dışarı çağırıyor ve claimRewards() re-enter edilebiliyor; ayrıca “withdraw sonrası” state varsayıyorsa şunlar olabilir:
- double reward claims
- cooldown atlatma
- reward debt muhasebesinin bozulması
Yaygın bir mitigasyon:
- muhasebe güncellemelerini tek bir internal fonksiyonda merkezileştirmek
- tüm state-mutating entry point’lerde nonReentrant’ı tutarlı uygulamak
- external interaction’ları execution’ın sonuna izole etmek
Soken reentrancy’yi genelde nasıl inceler (ne beklemelisiniz)
Soken audit’lerinde özellikle şunları yaparız: - external call graph çıkarır ve “reentry window”ları anotlarız - CEI uyumluluğunu ve mutex kapsamını doğrularız - entegrasyonları callback destekli standartlara (ERC777, ERC721/1155 safe transfers, ERC4626 vaults) karşı test ederiz - severity’yi doğrulamak ve fix’leri teyit etmek için Foundry’de exploit PoC’leri deneriz
Bu, saldırganların tek bir atomic transaction içinde pozisyon boyutlarını büyütebildiği ve edge case’leri zorlayabildiği flash-loan-attacks-defi’ye açık DeFi tasarımlarında özellikle kritiktir.
Sonuç: Reentrancy önlenebilir—composability için mühendislik yaparsanız
Reentrancy, DeFi default olarak composable olduğu için üst seviye bir smart contract riski olmaya devam ediyor: çağırdığınız “external contract” bir kullanıcı, hook’lu bir token, bir vault, bir router veya saldırgan kontrollü bir callback olabilir. Çözüm nadiren tek bir numaradır—disiplinli mühendislik gerekir: CEI sıralaması, guarded entry point’ler, external call’ları minimize etmek ve adversarial testing.
Staking, lending, governance, bridges ya da asset taşıyan herhangi bir contract geliştiriyorsanız, reentrancy’yi ilk günden bir tasarım kısıtı olarak ele alın—en sonda çıkan bir lint uyarısı olarak değil.
Soken (soken.io), vaults, staking, governance ve karmaşık entegrasyonlarda smart contract auditing & penetration testing ve DeFi security reviews ile ekiplerin production sistemlerini sağlamlaştırmasına yardımcı olur. Reentrancy odaklı bir inceleme (PoC’ler ve Foundry testleri dahil) istiyorsanız, bir audit planlamak ve güvenle yayına çıkmak için soken.io üzerinden bizimle iletişime geçin.