12 Şubat 2025'te zkLend [1], StarkNet üzerinde bir borç verme protokolü olan, birikimli oran (accumulator) mekanizmasının sofistike bir şekilde manipüle edilmesiyle yaklaşık 10 milyon dolarlık bir saldırıya uğradı. Saldırgan, teminat değerlerini yapay olarak şişirmek ve protokolden diğer varlıkları ödünç alarak kâr etmek için flaş krediler ile yuvarlama açıklarından yararlandı.
Ancak güvenlik perspektifinden ayrıntılı ve doğru bir teknik analizin hâlâ eksikliği söz konusudur. Diğer güvenlik araştırmacılarının değerli içgörüler sunan mevcut analizlerine karşın, özellikle saldırı analizine ilişkin bazı yanlış anlamalar devam etmektedir. zkLend'in daha sonra yayımladığı resmi olay sonrası rapor [2] basitleştirilmiş bir açıklama sunmakla birlikte ayrıntılı bir teknik analiz içermemektedir. Bu blogda, olayı netleştirmek amacıyla kapsamlı bir inceleme sunmayı hedefliyoruz.
Temel Çıkarımlar (TL;DR)
-
Bu olayın temel nedeni, aşağıdaki üç sorunun bir arada bulunmasından kaynaklanmaktadır:
- Boş piyasa başlatma, rastgele varlık yatırılmasına olanak tanır.
- zkLend'in flaş kredisindeki özel bağış mekanizması, kullanıcıların teminat bakiyelerini dinamik olarak ayarlamak için global bir ölçekleme faktörü olan birikimli oranın manipüle edilmesini mümkün kılar.
- Kesinlik kaybı, kesme nedeniyle oluşur. Klasik bölme işlemindeki kesinlik kaybının aksine, payda 1'den başlar ancak çok büyük bir değere şişirilir; bu durum, hisse tokenının yakılması sırasında hafife almaya yol açar.
-
Saldırgan, diğer kullanıcılar tarafından yatırılan wstETH'den kâr etmedi. Bunun yerine saldırgan, teminat bakiyesini manipüle etmek için açıklardan yararlandı; teminat bakiyesini 7.000 wstETH'nin üzerine çıkarmak için başlangıç sermayesi olarak küçük miktarda wstETH kullandı ve böylece piyasadan diğer varlıkları ödünç almayı mümkün kıldı.
İlerleyen bölümlerde önce zkLend hakkında bazı önemli arka plan bilgileri sunacak, ardından sorunların ve ilgili saldırının derinlemesine analizini yapacağız.
0x1 Arka Plan: zkLend'in Temel Protokolünü Anlamak
zkLend, StarkNet üzerinde teminatlı krediler ve flaş krediler gibi yaygın borç verme protokollerini destekleyen bir borç verme projesidir. Bu iki protokolün uygulama ayrıntılarına inelim.
0x1.1 Teminatlı Krediler
Teminatlı kredi, kullanıcıların diğer varlıkları ödünç almak karşılığında belirli varlıkları protokole teminat olarak yatırdığı süreci ifade eder. Teminatın değeri, borçlanma kapasitesini belirlemek için kullanılır. Borç verme protokollerinin genellikle teminatın varlık değerini doğrudan saklamadığını, bunun yerine aşağıdaki formülü kullanarak hesapladığını belirtmek gerekir:
teminat_bakiyesi = borç_verme_birikimli_oranı * ham_bakiye
Özellikle, borç_verme_birikimli_oranı, her kullanıcının teminat değerini dinamik olarak ayarlayan bir ölçekleme faktörüdür; ham_bakiye ise kullanıcının piyasadaki gerçek payını temsil eder. ham_bakiye, borç_verme_birikimli_oranı kullanılarak teminat_bakiyesinden türetilir.
Bu tasarımın amacı nedir? Protokolün teminat değerini verimli biçimde yönetmesini ve kullanıcıları varlık yatırmaya teşvik etmesini sağlar. Protokol kazançlarının bir bölümü teminat sağlayıcılara tahsis edilerek borç_verme_birikimli_oranı artar; bu da tüm kullanıcıların teminat değerini orantılı ve eş zamanlı olarak artırır.
0x1.2 zkLend'de Flaş Krediler
Flaş kredi, kullanıcıların protokolden çok kısa bir süre için, genellikle tek bir işlem içinde, teminatsız olarak varlık ödünç alabildiği bir kredi türüdür. Borçlunun krediyi geri ödememesi veya belirtilen koşulları karşılamaması durumunda işlemin tamamı geri alınır ve kredi gerçekleştirilmez.
zkLend'in flaş kredi uygulamasında benzersiz bir bağış mekanizması bulunmaktadır. Özellikle, kullanıcılar varlıkları geri öderken yalnızca gereken minimum miktarı iade etmekle kalmayıp bağış olarak ek fon da katkıda bulunabilirler. Protokol bu bağış yapılan fonları takip eder ve borç_verme_birikimli_oranını buna göre günceller. Bu süreç settle_extra_reserve_balance() fonksiyonunda uygulanmaktadır. borç_verme_birikimli_oranını güncelleme formülü aşağıdaki gibidir:
yeni_birikimli_oran = (rezerv_bakiyesi + toplam_borç - hazineye_giden_miktar) / ztoken_arzı
rezerv_bakiyesi: Sözleşmede tutulan toplam temel token miktarı (örn. wstETH); kullanıcılar tarafından bağışlanan token miktarını da içerir.toplam_borç: Tüm borçlu kullanıcıların toplam borcu.hazineye_giden_miktar: Protokol gelirinin miktarı.ztoken_arzı: Hisse tokenının toplam arzı (örn. zwstETH). Kullanıcılar wstETH yatırdığında zkLend ztoken sözleşmesi eşdeğer miktarda zwstETH basar.
zkLend'in temel protokolünü anladıktan sonra, saldırganın borç_verme_birikimli_oranı ve ham_bakiye değişkenlerini manipüle ederek teminat varlıklarını nasıl manipüle ettiğini ayrıntılı biçimde açıklayacağız.
0x2 Saldırı Analizi
Saldırgan, teminat değerini manipüle etmek için zkLend sözleşmesindeki aşağıdaki mekanizma ve açıklardan yararlandı:
borç_verme_birikimli_oranınınManipülasyonu- Boş piyasa: Saldırıdan önce wstETH tokenları için zkLend piyasası boştu; bu durum manipülasyon için mükemmel bir koşul yarattı. Üstelik zkLend Market sözleşmesi, herkesin boş bir piyasaya herhangi bir miktarda varlık yatırmasına olanak tanımaktadır. Saldırgan,
borç_verme_birikimli_oranıdeğerini önemli ölçüde şişirmek için küçük miktarda varlık yatırdı. - Bağış mekanizması: zkLend Market sözleşmesinin
flash_loan()fonksiyonu benzersiz bir bağış mekanizması içermektedir. Özellikle, bir kullanıcı flaş krediyi geri ödediğinde Market sözleşmesi fazladan iade edilen fonları hesaplar ve globalborç_verme_birikimli_oranıdeğişkenini artırarak sözleşmedeki tüm kullanıcıların teminat değerlerini yükseltir.
- Boş piyasa: Saldırıdan önce wstETH tokenları için zkLend piyasası boştu; bu durum manipülasyon için mükemmel bir koşul yarattı. Üstelik zkLend Market sözleşmesi, herkesin boş bir piyasaya herhangi bir miktarda varlık yatırmasına olanak tanımaktadır. Saldırgan,
ham_bakiyenin Manipülasyonu- Yuvarlama davranışı: Hisse tokenı yakma işlemindeki bölme operasyonu kesme kullanır; bu durum, çekme işlemlerinde kullanıcının
ham_bakiyesindeki değişikliğin hafife alınmasına yol açar.
- Yuvarlama davranışı: Hisse tokenı yakma işlemindeki bölme operasyonu kesme kullanır; bu durum, çekme işlemlerinde kullanıcının
Bu iki değişkeni manipüle ederek saldırgan, teminat bakiyesini 7.000 wstETH'nin üzerine çıkarmayı ve kâr amacıyla piyasadan diğer varlıkları ödünç almayı başardı.
0x2.1 lending_accumulator Değişkeninin Manipülasyonu
0x2.1.1 Boş Piyasa Başlatma
Market sözleşmesinin saldırı öncesindeki işlem kaydı incelendiğinde, saldırganın başlangıçta wstETH Market sözleşmesine 1 wei wstETH yatırdığı görülmektedir. Bu işlemin iç çağrıları incelendiğinde, wstETH Market sözleşmesinin 0 wstETH tuttuğu ve zwstETH'nin toplam arzının da 0 olduğu açıkça görülmektedir.
Bu nedenle, zkLend wstETH piyasasında daha önce herhangi bir yatırım veya borçlanma yapılmadığını doğrulayabiliriz. Hem rezerv_bakiyesi hem de ztoken_arzı başlangıç değerleri olan 0'daydı ve borç_verme_birikimli_oranının başlangıç değeri 1'di. Bu boş piyasa senaryosu, sonraki saldırı için gerekli koşulları yarattı ve saldırganın minimum miktarda wstETH ile borç_verme_birikimli_oranını önemli ölçüde artırmasına olanak tanıdı.
0x2.1.2 Flaş Kredi Aracılığıyla lending_accumulator'ın Manipülasyonu
Ardından bu işlemde saldırgan, 1 wei wstETH ödünç alıp 1000 wei wstETH geri ödeyen flash_loan() fonksiyonunu çağırdı. Fazladan iade edilen 999 wei, bir bağış olarak değerlendirildi ve sözleşmenin rezerv_bakiyesine kaydedildi.
borç_verme_birikimli_oranını hesaplama formülüne göre bu işlem, borç_verme_birikimli_oranının 1'den 851,0'a yükselmesine neden oldu.
0x2.1.3 flash_loan()'un Tekrarlı Çalıştırılması
Saldırgan toplam 10 kez flash_loan() çağrısı gerçekleştirdi; her seferinde yalnızca 1 wei wstETH ödünç aldı ancak daha büyük miktarlar geri ödedi. Sonuç olarak borç_verme_birikimli_oranı, wstETH'nin ondalık hassasiyetiyle tesadüfen örtüşen 4.069.297.906.051.644.020 (4,069 × 10^18) gibi astronomik bir değere yükseldi.
0x2.2 raw_balance Değişkeninin Manipülasyonu
borç_verme_birikimli_oranını yaklaşık 4,069 × 10^18'e yükselttikten sonra saldırgan, Market sözleşmesinin deposit() fonksiyonunu 4,069297906051644020 wstETH ile çağırdı. borç_verme_birikimli_oranının en güncel değerine göre, saldırı sözleşmesinin ham_bakiyesi 2 oldu.
0x2.2.1 ham_bakiye'yi Manipüle Eden İlk İşlem
Bu işlemde saldırgan, saldırı sözleşmesinin callflashloandraaan() fonksiyonunu çağırdı. Bu sözleşme açık kaynak olmamakla birlikte, iç çağrı izine dayanarak bu fonksiyonun mantığının aşağıdaki eylemleri gerçekleştiren bir döngü içerdiği tahmin edilebilir:
- Yatırma: Saldırgan, piyasa sözleşmesine belirli miktarda wstETH yatırır.
- Çekme: Saldırgan, belirli miktarda wstETH çeker.
Token Transfer Kaydı Analizi
Saldırganın yatırdığı wstETH miktarının her zaman borç_verme_birikimli_oranının tam bir katı olduğu gözlemlenebilir; örneğin değerin 2 katı (örn. 8,13859).
Ancak çekilen wstETH miktarı, borç_verme_birikimli_oranının 1,5 katıdır (örn. 6,10394).
Hesaplamalar aracılığıyla, çekilen wstETH miktarının yatırılan miktarı aştığını belirleyebiliriz. Bu neden olur?
Yuvarlama Davranışı
deposit() ve withdraw() metodlarının uygulaması incelendiğinde, bu iki metodun sırasıyla zwstETH basımı ve yakımını içerdiği görülmektedir. Bu nasıl çalışır:
Market sözleşmesindeki `mint()` fonksiyonu
Market sözleşmesindeki `burn()` fonksiyonu
mint() ve burn() süreçlerinin her ikisi de bir aşağı ölçekleme mantığı içermektedir. Aşağı ölçekleme mantığı, aşağı yuvarlama (en yakın tam sayıya aşağı yuvarlama) ile tam sayı bölmesini içerir ve bu durum açıkta kilit bir rol oynar.
Saldırgan belirli miktarda zwstETH yaktığında, aşağı ölçekleme mantığı uygulanır. Manipüle edilmiş borç_verme_birikimli_oranı değerinin son derece yüksek olması (yaklaşık 4.069.297.906.051.644.020) nedeniyle, bu bölme işlemi saldırganın ham_bakiyesini 6'dan fazla zwstETH yakmasına rağmen yalnızca 1 birim azaltır.
Saldırganın ham_bakiye değişimleri aşağıdaki tabloda özetlenmiştir:
Bu işlemde saldırganın Yatırma - Çekme mantığını tekrar tekrar uyguladığı ve withdraw() fonksiyonundaki kesinlik kaybından yararlandığı görülmektedir; bu durum ham_bakiye farkının hafife alınmasına neden olmaktadır. Sonuç olarak kullanıcının ham_bakiyesi 2'den 3'e yükseldi ve bir birim daha kazanıldı.
0x2.2.2 Sonraki Saldırı Süreci
Sonraki saldırı işlemleri, ilk saldırıyla aynı kalıbı izledi: saldırgan wstETH elde etmek için tekrar tekrar Yatırma - Çekme işlemleri döngüsünü uyguladı.
Elde edilen wstETH tekrar piyasaya yatırılarak ham_bakiye daha da artırıldı ve saldırganın teminat değerinin sürekli yükselmesi sağlandı.
Örnek açıklaması
Aşağıdaki işlemi örnek olarak kullanıyoruz.
- Toplam 30 yatırma işlemi gerçekleştirildi; her seferinde 4,069 wstETH yatırıldı.
- Toplam 30 çekme işlemi gerçekleştirildi; her seferinde 6,104 wstETH çekildi.
- Bu döngünün ardından saldırgan, hesaplamalara göre 61,39 wstETH başarıyla çıkardı.
Ayrıca bu saldırı işlemleri arasında birkaç increase() metodunun çağrıldığını belirtmek gerekir. Bu metodlar, saldırganın hesabından saldırı sözleşmesine belirli miktarda wstETH aktarmak için kullanıldı; bu fonlar daha sonra Market sözleşmesine yapılacak sonraki yatırımları finanse etti.
Bu eylemler ham_bakiye değerini artırarak saldırganın teminat değerini artırmaya devam etmesini sağladı. Sonunda saldırganın ham_bakiyesi 1.724'e ulaştı ve değeri 7.015,4 wstETH oldu; bu, piyasadan diğer varlıkları ödünç almak için yeterliydi.
0x3 Kâr Analizi
0x3.1 Diğer Türde Fonları Ödünç Alma
Teminat değerini manipüle ettikten sonra saldırgan, piyasadan diğer türde fonları ödünç aldı ve aşağıdaki işlemleri gerçekleştirdi (özet):
0x3.2 Ödünç Alınan Fonları Katman 1'e Köprüleme
Saldırganın sözleşmesinin köprü işlemleri incelendiğinde, saldırganın ödünç alınan fonların bir kısmını Katman 1'e köprülediği görülmektedir.
0x4 Sonuç
Özetle, zkLend protokolüne yönelik bu saldırı, merkezi olmayan borç verme protokollerinin tasarımı ve güvenliği açısından birkaç önemli sonucu gün yüzüne çıkarmaktadır:
- Piyasa Başlatma ve Varlık Yatırma Koşulları:
Başlangıçtaki boş piyasa, saldırganın küçük miktarda wstETH yatırmasına ve
borç_verme_birikimli_oranınımanipüle ederek açık için kaldıraç elde etmesine olanak tanıdı. Yeterli bir likidite tabanı sağlamak veya piyasanın erken aşamalarında varlık bağışlarını sınırlamak benzer saldırıların önlenmesine yardımcı olabilir. - Uygun Birikimli Oran Mekanizmalarının Önemi:
Saldırgan,
borç_verme_birikimli_oranınımanipüle etmek ve tüm kullanıcılardaki teminat değerlerini şişirmek içinflash_loan()fonksiyonundaki bağış mekanizmasından yararlandı. Birikimli oran tabanlı mekanizmalara sahip protokoller, ölçekleme faktörlerinin kolay manipülasyonuna karşı önlem almalıdır. - Yuvarlama Davranışı ve Kesinlik Kaybı:
zwstETH token yakımı sırasında ortaya çıkan bir yuvarlama sorunu, kesinlik kaybına ve
ham_bakiyenin hafife alınmasına yol açtı; bu durum saldırganınham_bakiyeyi manipüle etmesine olanak tanıdı. Protokoller bu tür açıkları önlemek için daha yüksek hassasiyet veya doğrulama kontrolleri kullanmalıdır.
Bu olay bir kez daha, başlatma ve operasyonel duruma ilişkin zamanında bildirimler yapılmasının ve olası kayıpları azaltmak amacıyla proaktif tehdit önlemenin önemini vurgulamaktadır.
Referans
[1] https://zklend.com/
[2] zkLend güvenlik olayı olay sonrası raporu: https://drive.google.com/file/d/10i1dh_J89tPPw7KRcmFIVM6iNrJZAyfi/view



