2 Eylül 2025 tarihinde Bunni V2 protokolü, gelişmiş bir saldırıya maruz kaldı [1]. Bir saldırgan, likidite muhasebesi mekanizmasındaki kritik bir açığı kullanarak iki likidite havuzundan yaklaşık 8,4 milyon USD çekti: Ethereum üzerindeki USDC/USDT havuzu [2] ve Unichain üzerindeki weETH/ETH havuzu [3].
Temel neden, protokolün likidite kaldırma işlemi sırasında boşta kalan havuz bakiyelerini güncellemesindeki bir yuvarlama hatasıydı. Bu hata, sözleşmedeki toplam likiditenin önemli ölçüde düşük değerlenmesine yol açarak teorik ve gerçek likidite arasında istismar edilebilir bir tutarsızlık yarattı. Saldırgan daha sonra bu farklılıktan kâr elde etmek için hassas bir sandviç saldırısı gerçekleştirdi.
Bu olay, Bunni protokolü için doğrudan ciddi bir mali kayıpla sonuçlandı; protokol akabinde 23 Ekim 2025 tarihinde iflasını ilan etti [4].
Arka Plan
Bunni V2, Uniswap V4 üzerine inşa edilmiş bir Otomatik Piyasa Yapıcı (AMM) protokolüdür. Temel mantığını kanca (hook) mekanizması aracılığıyla uygular ve Likidite Sağlayıcıları (LP'ler) için geliştirilmiş sermaye verimliliği sunmayı hedefleyerek Uniswap V3'ün yoğunlaştırılmış likidite algoritması üzerinde yenilikler getirir [5].
Protokol, LP getirilerini öncelikli olarak bir Rehipotekasyon (Rehypothecation) özelliği ve bir Yeniden Dengeleme (Rebalancing) mekanizması aracılığıyla artırır. İlki, likiditeyi harici getiri üreten protokollere tahsis ederek temel likiditeyi korurken ek harici getiri elde eder. İkincisi ise sermayenin aktif kullanımını artırarak ücret gelirini yükseltmek amacıyla likiditenin fiyat aralıklarına dağılımını sürekli optimize eder. Bu iki mekanizma, protokolün temel yoğunlaştırılmış likidite modeli üzerindeki temel yeniliklerini oluşturur.
Rehipotekasyon
Likidite Sağlayıcılarının getirilerini artırmak için Bunni V2, bir Rehipotekasyon stratejisi kullanır. Bu strateji, fonları farklı pozisyonlara tahsis eder:
- rawBalance: Bir token için havuzun rezervlerinin bir kısmı, doğrudan Uniswap V4'ün
contract PoolManageriçinde saklanır. Bu, takasları kolaylaştırmak için anında kullanılabilir likidite görevi görür. - reserves: Geri kalanı belirlenmiş bir ERC4626 kasasına yatırılır. Bu, kullanıcıların bu varlıklar üzerinde ek harici getiri elde etmesine olanak tanır.
Bu nedenle, bir havuzun toplam varlıkları şu şekilde tanımlanır: Havuz Varlıkları = rawBalance + reserves'in temel miktarı.
Yeniden Dengeleme
Ücret gelirini artırmak için Bunni V2, zaman ağırlıklı ortalama fiyatı izleyen bir yeniden dengeleme mekanizması uygular. Fiyat değişimi bir eşiği aştığında, likidite Likidite Dağılım Fonksiyonu'na (LDF) göre farklı fiyat aralıklarına yeniden dağıtılır.
Bu yeniden dağıtım, LDF tarafından gerektirilen token oranını değiştirebilir ve bir tokende fazlalık bırakabilir. Bu fazlalık, boşta kalan bakiye (idle balance) olarak tanımlanır.
Bu nedenle likidite iki bölüme ayrılır:
- Aktif Bakiye: LDF tarafından likidite hesaplamasına katılan tahsis edilmiş kısım.
- Boşta Kalan Bakiye: Aktif likidite için kullanılmayan fazlalık.
Bu nedenle, Havuz Varlıkları = Aktif Bakiye + Boşta Kalan Bakiye.
Temel fonksiyonlar: likidite hesaplama ve kaldırma
Bu saldırı iki kritik fonksiyonu istismar eder: queryLDF() ve withdraw(). queryLDF() fonksiyonu havuzun takaslar için likiditesini hesaplarken, withdraw() fonksiyonu kullanıcıların orantılı bir likiditeyi kaldırmasına olanak tanır.
queryLDF() Fonksiyonu
Rehipotekasyon stratejisi nedeniyle temel varlıkların miktarı dinamiktir ve Bunni V2 sabit bir "toplam likidite" değeri saklamaz. Bunun yerine protokol, bir takas gerçekleştiğinde gerçek zamanlı likiditeyi almak için queryLDF() fonksiyonunu sağlar [6]. Bu fonksiyonun yürütme süreci aşağıdaki dört adımdan oluşur:
-
Likidite Yoğunluğunu Sorgulama:
-
Mevcut fiyat tik aralığının dışındaki likidite yoğunluğunu elde eden
ldf.query()likidite yoğunluğu fonksiyonunu çağır. -
Mevcut tik aralığı içindeki yoğunluğu elde etmek için
LiquidityAmounts.getAmountsForLiquidity()fonksiyonunu çağır. -
Her iki yönde de token0 ve token1'in toplam likidite yoğunluğunu hesapla; bunu
totalDensity0vetotalDensity1olarak ifade et.
Özellikle,
LiquidityAmounts.getAmountsForLiquidity()fonksiyonu, hesaplanan token miktarlarının teorik değerlerden ihtiyatlı biçimde az olmadığını sağlamak için yukarı yuvarlama kullanır.
-
-
Kullanılabilir Bakiyeyi Hesapla
Likidite hesaplamalarında kullanılan kullanılabilir bakiyeler
balance0vebalance1olarak ifade edilir. Boşta kalan bakiye, likidite hesaplamalarına katılmayan fonları hariç tutarak ilgili tokenin toplam bakiyesinden düşülür.Bu saldırıda, havuzun boşta kalan fonlarının
token0'dan oluştuğu durumda hesaplama formülleri şöyledir: -
-
Efektif Likiditeyi Tahmin Et
-
Her tokenin gerçek kullanılabilir bakiyesine (
balance0veyabalance1) ve hesaplanan toplam yoğunluğuna (totalDensity0veyatotalDensity1) dayanarak her tokenin destekleyebileceği likiditeyi tahmin et. -
İki tahminin küçüğünü nihai efektif toplam likidite olarak seç.
Formül şu şekildedir:
-
-
Aktif Bakiyeleri Hesapla
Belirlenen toplam likiditeye dayanarak protokol, işlem için mevcut olan gerçek token miktarını hesaplar. Bu, Aktif Bakiye olarak tanımlanır.
withdraw() Fonksiyonu
Bunni V2, likidite kaldırmak için withdraw() fonksiyonunu sağlar. Kullanıcılar, havuzun toplam fonlarındaki paylarıyla orantılı likiditeyi kaldırır. Protokol, rawBalance, reserves ve idleBalance değerlerini aynı oranda günceller. Düzeltme formülü şu şekildedir:
Burada:
shares, kullanıcının kaldırdığı likidite payı sayısıdır;totalSupply, o havuzun likidite tokenlerinin toplam arzıdır.
Güvenlik Açığı Analizi
Güvenlik açığı, withdraw() fonksiyonunun boşta kalan bakiye için düzeltme miktarını hesaplarken aşağı yuvarlama (yani, sıfıra doğru yuvarlama) kullanmasından kaynaklanmaktadır. Bu durum boşta kalan bakiyenin fazla tahmin edilmesine yol açar.
Kullanılabilir bakiye formülü hatırlandığında, . Fazla tahmin edilmiş bir boşta kalan bakiye, likidite hesaplamalarında kullanılan kullanılabilir bakiyenin (balance0) doğrudan düşük tahmin edilmesine neden olur. Sonuç olarak, tahmini efektif toplam likidite de düşük değerlenir. Bunni Saldırı Olay Sonrası Raporuna göre [7], likidite hesaplamalarındaki bu yuvarlama yönü kasıtlı olarak kullanılmıştır. Daha düşük hesaplanan bir likidite değeri, takaslar sırasında daha yüksek bir fiyat etkisine yol açar.
Bu tasarım kritik bir varsayıma dayanır: İki token arasındaki bakiye oranı görece dengeli kalır. Normal koşullarda yeterli likidite ile her iki token için ayrı ayrı tahmin edilen toplam likidite değerleri genellikle birbirine yakındır. Bu nedenle yuvarlama hatasının etkisi sınırlıdır. Ancak boşta kalan bakiyeyi taşıyan tokenin kullanılabilir bakiyesi son derece düşük olduğunda, bu kusur ortaya çıkar. Bu senaryoda, aşağı yuvarlama hatası önemli ölçüde büyütülür.
Saldırgan bu açığı kullanarak bir dizi küçük çekim gerçekleştirdi ve aşağı yuvarlama yoluyla token0'ın kullanılabilir bakiyesini 28 wei'den 4 wei'ye düşürdü. Bu düşüş, fiilen yakılan likidite paylarına karşılık gelen teorik oranı (yani %8,998105442969973e-07) çok aştı. Bu arada token1'in kullanılabilir bakiyesi görece normal bir seviyede kaldı. Bu dengesizlik önemli bir arbitraj fırsatı yarattı. Bir sonraki bölümde ayrıntılı bir sayısal analiz sunulmaktadır.
Saldırı Analizi
Ethereum işlemi [2] örnek alındığında, saldırgan üç aşamalı bir saldırı gerçekleştirdi:
- Birinci aşamada saldırgan, USDC'nin kullanılabilir bakiyesini (token0) önemli ölçüde tüketmek için fiyat manipülasyonu yaptı. Bu, sonraki yuvarlama hatasını büyütmek için gerekli başlangıç koşullarını yarattı.
- İkinci aşamada, bir dizi küçük çekim yoluyla temel istismar gerçekleştirildi ve protokolün havuzun gerçek likiditesini düşük tahmin etmesine neden olundu.
- Üçüncü aşamada saldırgan, protokolün düşük tahmin ettiği likidite ile havuzun gerçek likiditesi arasındaki tutarsızlıktan arbitraj yapmak için iki yönlü takas gerçekleştirdi ve nihayetinde kâr elde etti.
Aşama 1: Fiyatı manipüle etme ve hedef token bakiyesini azaltma
Saldırgan üç takas işlemi gerçekleştirerek USDC'nin (token0) USDT'ye (token1) göre fiyatını manipüle etti ve başlangıç tik = -1'den tik = 5000'e itti. Temel amaç, havuzun aktif USDC bakiyesini tüketmek ve onu 28 wei gibi son derece düşük bir seviyeye indirmekti. Bu, sonraki aşamada gerçekleştirilecek yuvarlama hatasını büyütmek için gerekli başlangıç koşullarını yarattı.
Aşama 2: Likidite tutarsızlıklarını büyütmek için çekimleri istismar etme
Saldırgan, withdraw() fonksiyonu aracılığıyla 44 küçük çekim başlattı. Bu fonksiyonun idleBalance'ı güncellerken kullandığı aşağı yuvarlama nedeniyle protokolün boşta kalan bakiyesi fazla tahmin edildi. Bu durum, queryLDF() fonksiyonundaki USDC kullanılabilir bakiyesini daha da düşük tahmin etti. Bu tekrarlanan işlemler sonucunda USDC kullanılabilir bakiyesi 28 wei'den anormal biçimde 4 wei'ye düşürüldü. Bu, kaldırılan likidite paylarına karşılık gelen teorik oranı (yani %8,998105442969973e-07) çok aşan gerçek bir %85,7 azalma temsil ediyordu. Bu noktada havuzdaki USDC'den tahmin edilen likidite ciddi ölçüde düşük değerlenmişti.
Aşama 3: Arbitrajı gerçekleştirme ve kârı realize etme
Saldırgan daha sonra bir sandviç saldırısına benzer bir işlem oluşturan iki yönlü takas gerçekleştirdi.
Adım 1: Saldırgan, büyük miktarda USDT kullanarak USDC satın aldı. Bu sırada dahili likidite hesaplaması, düşük tahmin edilen USDC bakiyesine dayanarak ciddi ölçüde düşük değerlenmişti. Bu büyük takas fiyatı aşırı bir noktaya itti ve tiki 5.000'den 839.189'a taşıdı.
Adım 2: Aşırı fiyat oluştuktan sonra saldırgan hemen işlemi tersine çevirerek bir kısım USDC'yi tekrar USDT ile takas etti. Havuzun fiyatı artık ciddi ölçüde yanlış hizalandığından, queryLDF() fonksiyonunun USDC likidite yoğunluğu için döndürdüğü değer 1'e düştü. Bu, USDC'ye dayanarak tahmin edilen likidite değerini USDT'ye dayanarak tahmin edilen değerden büyük hale getirdi.
Protokolün daha küçük değeri seçme mantığına göre toplam likidite, USDT bakiyesi tarafından belirlenir. Bu durum, hesaplanan likiditenin düşük değerlenmiş bir durumdan normal bir seviyeye anında dönmesine ve ani bir artışa yol açmasına neden oldu. Saldırgan bu değişimi kullanarak minimum miktarda USDC karşılığında büyük miktarda USDT elde etti ve böylece arbitrajı tamamlayarak kâr elde etti.
Özet
Bu olay nihayetinde likidite kaldırma işlemi sırasında boşta kalan bakiyelerin ayarlanmasındaki yuvarlama hatalarından kaynaklandı. Bu aşağı yuvarlama fonksiyonu tasarımı likidite hesaplamalarında bir güvenlik stratejisi olarak amaçlanmış olsa da kritik sınır koşullarını yeterince gözetmedi. Özellikle, token bakiyeleri ciddi ölçüde dengesiz olduğunda yuvarlama hataları doğrusal olmayan biçimde büyütülür.
Bu olay, karmaşık DeFi protokollerinde birden fazla modül arasındaki bağlantı risklerini gözler önüne serdi. Bireysel bileşenlerin yuvarlama kuralları ihtiyatlı biçimde tasarlanmış olsa bile tüm sistemde tutarlı güvenlik doğrulamasının eksikliği, belirli koşullar altında istismar edilebilecek kritik güvenlik açıklarına yol açabilir.
Referans
-
https://etherscan.io/tx/0x1c27c4d625429acfc0f97e466eda725fd09ebdc77550e529ba4cbdbc33beb97b
-
https://uniscan.xyz/tx/0x4776f31156501dd456664cd3c91662ac8acc78358b9d4fd79337211eb6a1d451
BlockSec Hakkında
BlockSec, tam yığın blok zinciri güvenliği ve kripto uyumluluk sağlayıcısıdır. Müşterilerin kod denetimi gerçekleştirmesine (akıllı sözleşmeler, blok zinciri ve cüzdanlar dahil), saldırıları gerçek zamanlı olarak engellenmesine, olayları analiz etmesine, yasadışı fonları takip etmesine ve protokollerle platformların tam yaşam döngüsü boyunca AML/CFT yükümlülüklerini yerine getirmesine yardımcı olan ürünler ve hizmetler geliştiriyoruz.
BlockSec, prestijli konferanslarda birden fazla blok zinciri güvenlik makalesi yayımlamış, DeFi uygulamalarının birkaç sıfırıncı gün saldırısını raporlamış, 20 milyon dolardan fazlasını kurtarmak için birden fazla saldırıyı engellemiş ve milyarlarca değerinde kripto parayı güvence altına almıştır.
-
Resmi web sitesi: https://blocksec.com/
-
Resmi Twitter hesabı: https://twitter.com/BlockSecTeam



