Back to Blog

Hassasiyet Kaybının Bir Diğer Trajedisi: KyberSwap Olayının Derinlemesine Analizi

Code Auditing
December 5, 2023
10 min read

23 Kasım 2023'te KyberSwap'i hedef alan bir dizi saldırı gözlemledik. Bu saldırılar sonucunda toplam 48 milyon doların üzerinde kayıp yaşandı. İlk analizimiz, açığın tick manipülasyonu ve çift likidite sayımından kaynaklandığını öne sürdü. Ancak alan kısıtlamaları nedeniyle o yazıda kapsamlı ayrıntılara giremiyorduk. Diğer güvenlik araştırmacılarının sonraki aydınlatıcı analizlerine rağmen, sorunun temel nedeni olan hassasiyet kaybı gün yüzüne çıkmamıştı.

İlginç bir şekilde, birkaç gün sonra olay daha da girift bir hal aldı. 30 Kasım 2023'te, yetkililerle birden fazla tur müzakerenin ardından saldırgan, dış dünyaya kışkırtıcı görünen ve tam kontrol talep eden bir mesaj gönderdi. Bunu bir kenara bırakırsak, saldırgan aynı zamanda kritik bir bilgiyi de ortaya koydu: sorun gerçekten de hassasiyet kaybıyla ilgilidir; aşağıdaki şekilde gösterildiği gibi. Bu açıklama, soruşturmamıza ilişkin kanıtları pekiştirmektedir. Bu nedenle amacımız, bu raporda kapsamlı bir analiz sunmaktır.

Temel Çıkarımlar (Özet)

  • Araştırmamız, temel sorunun KyberSwap'in yeniden yatırım sürecinde yanlış yuvarlama yönünden kaynaklandığını ortaya koymaktadır. Bu durum daha sonra hatalı tick hesaplamasına ve nihayetinde çift likidite sayımına yol açmaktadır.

  • Bu olay, DeFi protokollerindeki hassasiyet kaybı sorunlarının karmaşık ve gizli doğasını gözler önüne sermekte ve tüm topluluk için ciddi bir meydan okuma oluşturmaktadır.

  • Bu saldırıların sıklığı, proaktif tehdit önleme tedbirlerine duyulan kritik ihtiyacın çarpıcı bir hatırlatıcısı olmakta; bu tedbirler gelecekteki kayıpların azaltılmasına önemli ölçüde katkı sağlayabilir.

Aşağıdaki bölümlerde önce KyberSwap hakkında bazı önemli arka plan bilgileri sunacağız. Ardından güvenlik açığı ve buna bağlı saldırının derinlemesine analizini gerçekleştireceğiz.

0x1 Arka Plan

KyberSwap[1], merkeziyetsiz bir otomatik piyasa yapıcı (CLAMM) platformudur. Yoğunlaştırılmış likidite piyasa talebini karşılamak amacıyla KyberSwap Elastic[3], Uniswap V3[2] temel alınarak geliştirilmiş ve likidite sağlama getirilerinin otomatik bileşik faizini mümkün kılan yeniden yatırım eğrisi dahil çeşitli iyileştirmeler içererek hayata geçirilmiştir.

0x1.1 Tick ve Karekök Fiyat

Uniswap V3 benzeri CLAMM'lerde Tick, fiyatı ayrık bir biçimde işaretlemek için kullanılır; böylece LP'ler tüm aralık yerine belirli bir aralıkta likidite sağlayabilir (dolayısıyla "yoğunlaştırılmış" terimi)[4].

LP'lerin özelleştirilmiş fiyat aralıklarıyla likidite pozisyonu belirleyebilmesi için protokolün çeşitli fiyat noktalarındaki toplam likiditeyi takip edecek bir yönteme ihtiyacı vardı. Uniswap V3, olası fiyat uzayını ayrık "tick"lere bölerek bunu başardı; LP'ler herhangi iki tick arasında likidite katkısı sağlayabilir.

[5]'e göre, likidite herhangi iki tick arasında (bitişik olmak zorunda değil) bir aralığa, yani bir çift tick indeksine (alt tick ve üst tick) yerleştirilebilir. Özellikle, her tick'in fiyatı (i tam sayı indeksinde) aşağıdaki gibi tanımlanır:

Pratikte karekök fiyat (sqrtP veya sqrtPrice olarak gösterilir) kullanılır:

Mevcut karekök fiyata göre mevcut tick'i hesaplamak da mümkündür:

Karekök fiyatı L likiditesiyle birlikte kullanmak, eş zamanlı değişiklikleri önlemenin pratik bir yoludur. Özellikle, fiyat bir tick içinde takas yapılırken değişir; likidite ise bir tick geçildiğinde ya da likidite eklenip çıkarıldığında değişir. Daha ayrıntılı bir açıklama için Uniswap V3'ün teknik belgesine[5] başvurabilirsiniz.

Açıkça görüldüğü üzere, belirli bir tick için yalnızca tek bir karekök fiyat hesaplansa da birden fazla karekök fiyat aynı tick'e işaret edebilir.

0x1.2 Yeniden Yatırım Eğrisi

Uniswap V3 tabanlı CLAMM, LP ücretlerinin havuz kullanımı ve yeniden yatırım için gereken yüksek gas ücretleri sorunuyla karşı karşıyadır. Bu nedenle KyberSwap, sorunu çözmek için yeniden yatırım eğrisini[6] benimsedi:

Yeniden yatırım eğrisi, yoğunlaştırılmış likidite modelinde aksi takdirde kullanılmayan LP ücretlerini yerel olarak yeniden yatırmak amacıyla tasarlandı. Bu sayede yoğunlaştırılmış likidite pozisyonlarına ait LP ücretleri, gas masrafı ya da manuel yönetim gerektirmeksizin otomatik olarak bileşik faize dönüştürüldü. Bunun yanı sıra, LP'ler otomatik bileşik faize dönüştürülmüş kazançlarını istedikleri zaman ayrıca tahsil etme seçeneğine sahiptir.

Yeniden yatırım eğrisinin özü, her takaste toplanan ücretlerin sonsuz bir aralıktaki yeniden yatırım likiditesi olarak havuza ek likidite şeklinde birikmesidir. Yeniden yatırım token'ları LP'lere basılır ve biriken yeniden yatırım likiditesi buna göre LP'lere dağıtılır. Bunun yanı sıra yeniden yatırım likiditesi, takas ve fiyat hesaplama sürecine de katılmaktadır.

Daha kesin ifade etmek gerekirse, sabit çarpım formülü yerine:

ücretler her takaste ΔL olarak biriktirilir:

ΔL hesabı şu şekilde basitleştirilebilir (fiyat sapmasının bir eşiğin altında olduğu varsayımıyla):

Ardından, takas miktarı ve nihai fiyat değiştirilmiş sabit çarpım formülünden türetilebilir:

Yukarıda tanıtılan hesaplamalara ait kod, ilgili havuzun aşağıdaki kod parçasındaki computeSwapStep fonksiyonunda gösterilmektedir.

Yeniden yatırım likiditesi nedeniyle bu fonksiyondaki liquidity değerinin iki bileşenin toplamı olduğuna dikkat edilmelidir: temel likidite için baseL ve yeniden yatırım için biriken likidite için reinvestL.

0x1.3 KyberSwap'te Takas

Uniswap V3'teki bir takasın kontrol akışı aşağıdaki gibi gösterilebilir[5]:

Buna göre, daha önce ele alınan KyberSwap havuzunun swap fonksiyonunun uygulaması aşağıdaki şema ile özetlenebilir:

Tick hesaplamasına ilişkin kritik mantık, mavi dikdörtgenle vurgulanan takas döngüsü içinde yer almaktadır. Özellikle, temel mantık computeSwapStep fonksiyonunu ve _updateLiquidityAndCrossTick fonksiyonunu kapsamaktadır. Birincisi, belirli bir takas için girdi ve çıktı miktarları ile nextSqrtP gibi temel durumları hesaplarken, ikincisi tick geçişi yaşandığında devreye girer.

Geleneksel olarak fiyat yükseldiğinde tick'in sağa/yukarıya kaydığını söyleriz; aksi halde tick sola/aşağıya hareket eder.

Daha sonra ele alınacak güvenlik açığını daha iyi anlayabilmek için, aşağıdaki şekilde gösterilen computeSwapStep fonksiyonunun ilgili kod mantığını incelememiz gerekmektedir:

Öncelikle, 50 ile 57. satırlar arasında calcReachAmount fonksiyonu çağrılarak targetSqrtP'ye (sonraki tick veya kullanıcı tarafından belirtilen hedef fiyat) ulaşmak için gereken girdi token miktarı hesaplanır.

Ardından, 59 ile 62. satırlar arasında tick'in geçilip geçilmeyeceğine dair bir test yapılır.

Özellikle, kullanılan miktar (usedAmount), tam girdi takasında (saldırıda kullanılan durum) kullanıcının belirttiği miktardan (specifiedAmount) fazlaysa tick geçilmemesi gerektiği ve nextSqrtP'nin artımlı likididen (deltaL, yani delta likidite) türetilmesi gerektiği anlamına gelir.

  • Ardından, 70 ile 79. satırlar arasında ΔL (deltaL), estimateIncrementalLiquidity fonksiyonu kullanılarak girdi miktarı, mevcut likidite ve fiyattan türetilir. Son olarak, takasın ardından oluşan nihai fiyat nextSqrtP, calcFinalPrice fonksiyonu kullanılarak deltaL, girdi miktarı, mevcut fiyat ve likiditeye göre hesaplanır.

Öte yandan, gerekli miktar kullanıcının belirttiği miktardan az ise (nextSqrtP > 0 anlamına gelir) deltaL, mevcut ve hedef sqrtP kullanılarak hesaplanır ve nextSqrtP, bir sonraki tick'teki sqrtP olur. Bu dal saldırıda kullanılmadığından ayrıntılar atlanmıştır.

Yukarıda özetlenen adımlardan açıkça anlaşılmaktadır ki tick geçilmezse computeSwapStep tarafından döndürülen nextSqrtP, bir sonraki tick'in sqrtP'sinden büyük olmamalıdır. Ancak fiyatın likiditiye (temel likidite ve delta likidite) olan bağımlılığı ve hassasiyet kaybı nedeniyle saldırganlar, tick geçilmemesine rağmen nextSqrtP'yi daha büyük bir değere manipüle edebilmektedir.

0x2 Güvenlik Açığı Analizi

Temel neden, SwapMath sözleşmesinin (computeSwapStep fonksiyonu tarafından çağrılan) delta likidite hesaplamasındaki (yani estimateIncrementalLiquidity fonksiyonu) hatalı yuvarlama yönünden kaynaklanan bozuk tick hesaplamasında yatmaktadır. Bu durum, daha sonra tick hesaplamasını da hatalı biçimde etkilemektedir.

İlginç bir şekilde, 188. satırdaki yorumu (mavi dikdörtgenle vurgulanan) incelediğimizde, nextSqrtP'yi aşağı yuvarlamak amacıyla deltaL'nin yukarı yuvarlanmasının amaçlandığını görürüz. Ancak 189. satırda mulDivFloor fonksiyonunun kullanılması nedeniyle deltaL yanlışlıkla aşağı yuvarlanmaktadır. Sonuç olarak nextSqrtP hatalı biçimde yukarı yuvarlanmaktadır.

0x3 Saldırı Analizi

Saldırganlar birden fazla saldırı işlemi başlattı; her işlemde birden fazla havuz boşaltıldı. Konuyu basitleştirmek adına aşağıdaki tartışma, saldırı işlemindeki ilk saldırıya dayanmaktadır.

Temel saldırı mantığı aşağıdaki altı adımdan oluşmaktadır:

  1. AAVE'den flash loan aracılığıyla 2.000 WETH borç almak.

  2. Kurban havuz 0xfd7b'de 6,850 WETH'i 6,371 frxETH ile takas etmek. Bu adım, mevcut tick'i ve currentSqrtP'yi şu anda likiditenin bulunmadığı bir konuma itmek için kullanılır.

  • currentSqrtP saldırgan tarafından rastgele seçilmiş gibi görünmektedir ve takas tam olarak bu fiyatta durur.
  • Bu adımın ardından temel likidite (baseL) sıfırken yeniden yatırım likiditesi (reinvestL) sıfır değildir.
  1. Havuza likidite eklemek ve ardından likit itenin bir kısmını çıkarmak. Bu adım, aralığı ve toplam likiditeyi istenen miktara ayarlamak için kullanılır.
  • Tick aralığı currentSqrtP'ye göre belirlenir.
  • Saldırı için istenen likidite tick aralığından türetilebilir, ancak buna karşılık gelen hesaplama mantığı daha fazla araştırma gerektirmektedir.
  1. Havuzda 387.170 WETH'i 0,06 frxETH ile takas etmek. Bu adım, mevcut tick'i nextTick == currentTick olacak şekilde manipüle etmek için kullanılır.
  • Girdi miktarı likidite ve currentSqrtP'ye göre belirlenir.
  1. Havuzda 0,06 frxETH'i 396.244 WETH ile takas etmek. Takas yönünün bir önceki adıma göre ters olduğuna dikkat edin. Bu adımda, takası kârlı hale getirmek ve havuzu boşaltmak amacıyla likidite çift sayılmaktadır.

  2. Flash loan'ı geri ödemek ve 6,364 WETH ile 1,117 frxETH kâr elde etmek.

Açıkça görüldüğü üzere, son iki takas (adım 4 ve adım 5), tick hesaplamasını manipüle etmek ve takası kârlı hale getirerek havuzu boşaltmak için kritik saldırı adımlarıdır. Ayrıntılara aşağıdaki alt bölümlerde değineceğiz.

Adım 3'ün likiditeyi manipüle etmek açısından kritik öneme sahip olduğunu belirtmek gerekir. Yuvarlama işlemi aracılığıyla hassas tick manipülasyonu gerektiğinden, doğrudan likidite ekleyerek bu amaca ulaşmak mümkün değildir. Likidite çıkarma işlemi, aralıktaki likiditeyi saldırganın istediği şekilde hassas biçimde kontrol etmek içindir.

0x3.1 Adım 4: Mevcut Tick'i ve currentSqrtP'yi Manipüle Etmek

Önceki adımların (adım 1 ve 2) ardından saldırgan, manipülasyon için tick aralığını ve likiditeyi hazırlamıştır. Özellikle:

  • currentSqrtP istenen konumdadır
  • mevcut tick = 110.909 ve sonraki tick = currentSqrtP'yi çevreleyen 111.310

Bu adım WETH'i frxETH ile takas eder. computeSwapStep fonksiyonunda aşağıdaki yürütme izine sahibiz:

Yukarıdaki şekilde gösterildiği gibi, hedefe (yani sonraki tick'e) ulaşmak için gereken miktar calcReachAmount fonksiyonu çağrılarak hesaplanacaktır:

  • usedAmount = calcReachAmount(liquidity, currentSqrtP, targetSqrtP)

Bu hesaplamanın takastan önce türetilebileceğine dikkat edin. specifiedAmount'u dikkatli biçimde seçerek (usedAmount = specifiedAmount + 1), saldırgan takası hedefe (yani sonraki tick 111.310'a) ulaşılmayacak şekilde kontrol etti; bu da nextSqrtP = 0 sonucunu doğurdu.

Bu durumda tick geçilmediğinden nextSqrtP (yani nihai fiyat), delta likididen (takas ücreti olarak biriken) türetilmesi gerekir.

Önce ücretlerden elde edilen artımlı likidite deltaL şu şekilde hesaplanır:

  • deltaL = estimateIncrementalLiquidity(absDelta, currentSqrtP)

Ardından nihai fiyat nextSqrtP:

  • nextSqrtP = calcFinalPrice(absDelta, liquidity, deltaL, currentSqrtP)

Önceki bölümde ele alınan yuvarlama yönü hatasını yeniden değerlendirdiğimizde, burada deltaL'nin hatalı biçimde aşağı yuvarlandığı ve bunun nextSqrtP'nin yukarı yuvarlanmasına yol açtığı görülmektedir. Özellikle bu durumda, aynı absDelta (387.170.294.533.119.999.999) temel alındığında, hesaplama sonuçları farklı yuvarlama yönleri nedeniyle ayrışmaktadır:

Bu nedenle adım 4'teki tick manipülasyonunun ardından mevcut durumlar aşağıdaki gibi özetlenebilir:

  • currentSqrtP, tick 111.310'daki sqrtP'den (111.310'daki sqrtP = 20.693.058.119.558.072.255.662.180.724.088) biraz daha büyük olan 20.693.058.119.558.072.255.665.971.001.964'tür.
  • mevcut tick = 111.310 ve sonraki tick = 111.310

Yukarıdaki şekilde gösterildiği gibi, adım 4'teki takas, havuzu tick 111.310'un geçilmediğine inandırmak için ustaca bir aldatmaca gerçekleştirir. Ancak gerçekte currentSqrtP, tick 111.310'un sqrtP'sinden büyüktür.

0x3.2 Adım 5: Likiditeyi Çift Saymak

Adım 4'teki manipülasyona dayanarak, adım 5'teki saldırı mantığı makul ölçüde basittir. Bu aşamada saldırgan, tick'i ve currentSqrtP'yi sola kaydıracak biçimde frxETH'ten WETH'e ters yönlü bir takas düzenledi. Özellikle computeSwapStep fonksiyonu döngü içinde iki kez çağrıldı; bu durum nihayetinde beklenmedik bir şekilde çift likidite sayımını[7] tetikledi ve ek kâr elde edilmesini sağladı.

Yukarıdaki izde gösterildiği gibi:

  • computeSwapStep fonksiyonunun ilk çağrısında currentSqrtP, tick 111.310'un sqrtP'sine kaydırıldı. Bu, tick 111.310'a gerçekten ulaşmak için yalnızca 3 wei frxETH kullanan küçük bir takasdır. Ardından _updateLiquidityAndCrossTick fonksiyonu içinde, adım 4'te sağa/yukarıya doğru tick 111.310 gerçek anlamda geçilmemiş olmasına rağmen mevcut tick, tick 111.310'u geçmek durumunda kalır (sola/aşağıya hareket ederek). Bu durum, tick 111.310'daki likit itenin iki kez sayılmasına yol açar.

  • computeSwapStep fonksiyonunun ikinci çağrısında, önceki çift likidite sayımı ek kâr potansiyeli doğurabilir. Özellikle bu likidite çift sayımından yararlanılarak son adımdaki takas fiyatı çarpıtılır ve daha fazla miktarda WETH takas edilmesine, dolayısıyla kâr elde edilmesine imkân tanınır.

0x4 Saldırıların ve Kârların Özeti

Bu yazının hazırlandığı sırada, vahşi ortamda farklı zincirlerde (Ethereum, Optimism, Polygon, Arbitrum, Avalanche ve Base dahil) birden fazla saldırı gözlemledik; bu saldırılar 48 milyon doların üzerinde kayba yol açtı. Bu saldırılar farklı saldırganlar tarafından başlatıldı:

Bu saldırı işlemlerinin tam listesi, hazırladığımız bir belgede bir araya getirilmiştir. Daha ayrıntılı bilgi için lütfen bu belgeye başvurun.

0x5 Sonuç

Sonuç olarak bu, hatalı yuvarlama mantığından kaynaklanan ince bir güvenlik açığıdır. Açık son derece sofistike bir yapıya sahiptir. Nitekim bu yıl DeFi topluluğu için ciddi zorluklar ortaya koyan hassasiyet kaybı sorunlarıyla ilgili bir dizi güvenlik olayı gözlemledik.

Bir kez daha, bu sürekli saldırılar potansiyel kayıpları azaltmaya etkin biçimde yardımcı olabilecek bir strateji olan proaktif tehdit önlemenin önemini ortaya koymaktadır.

Referanslar

[1] https://docs.kyberswap.com/

[2] https://blog.uniswap.org/uniswap-v3

[3] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic

[4] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/tick-range-mechanism

[5] https://uniswap.org/whitepaper-v3.pdf

[6] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/reinvestment-curve

[7] https://100proof.org/kyberswap-post-mortem.html

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit