Yazar: Jiujiu
Düzenleme: 77
Arka plan
4 Eylül 2026 tarihinde, ünlü merkeziyetsiz kredi platformu Notional Finance saldırıya uğradı ve yaklaşık 1,73 milyon ABD doları kaybetti. Aşağıda, bu saldırı olayına ilişkin SlowMist Güvenlik Takımı'nın ayrıntılı analizi yer almaktadır:
Ön bilgi
Notional Finance V1'de, fCash, son ödeme tarihi olan bir nakit alacak olarak anlaşılabilir. Nakit alıcı, vade gününde ödeme alır; nakit ödeyici ise vade gününde ödeme yapar. Protokol, her iki pozisyonu da Portföy'de kaydeder ve bir hesabın pozisyon kurma sağlığına serbest teminat kontrolü ile karar verir.
ERC1155Trade'nin safeTransferFrom fonksiyonu, varlık kaydı işlevini yerine getirir. Girilen varlık türü cash receiver ise, sözleşme Portfolios.mintfCashPair'i çağırır ve aynı zamanda ödemeyi yapan kişiye bir borç, alıcıya eşit tutarda bir alacak kaydeder. Bu çağrı ERC1155 transferi gibi görünse de, sonuç fCash çiftinin oluşturulmasıdır.
Yeni pozisyon Portfolio'ya yazıldıktan sonra serbest teminat hesaplaması ve kontrolü gerekmektedir. RiskFramework sözleşmesi, önce payer'ın fCash borçlarını imzalı int256 olarak toplar, ardından her bir varlık bakiyesini ETH cinsinden dönüştürerek toplar. Negatif sayılar ödenmesi gereken nakdi, pozitif sayılar ise hesabın sahip olduğu nakdi veya alacakları temsil eder. Bu hesaplama, işlemin yapılmasına izin verilip verilmeyeceğini belirler.
Pozisyon sona erdikten sonra, Portfolio, sona eren fCash'i nakit bakiyeye dönüştürmek için Escrow'un portfolioSettleCash fonksiyonunu çağırır. Hesap, ardından ilgili DAI veya USDC varlıklarını Escrow'dan çekmek için çekim fonksiyonunu kullanır.
Temel neden
Bu saldırıda temel zafiyet, Escrow uygulama sözleşmesinde kullanılan ExchangeRate._convertToETH fonksiyonunda, imzalı tamsayının mutlak değeri doğrudan uint128'e dönüştürülen bir kod parçası bulunmaktadır.

Saldırgan, önce aynı ödemeci için iki borç pozisyonu oluşturdu, miktarlar sırasıyla 1 ve uint128.max idi. Bu iki borç toplandığında tam olarak 2^128 sonucunu verdi. Ödemecinin risk hesaplamasındaki ödemeleri negatif olarak kaydedildiği için, Escrow'un convertBalancesToETH fonksiyonuna geçirilen kritik parametre -2^128 oldu.
Ancak bu sözleşme tarafından kullanılan Solidity sürümü 0.6.x olduğundan, mutlak değer 2^128, uint128'e zorla dönüştürüldükten sonra taşma nedeniyle 0 olur ve bu borç ETH cinsinden sonuçlara dahil edilmez.
Son olarak, mintfCashPair fonksiyonunda yalnızca ödemeyi yapan tarafın serbest teminatının sıfıra eşit veya daha büyük olması gerektiği için, sonuç sıfıra taştıktan sonra bu kontrolü geçebilir ve yeterli varlığa sahip olmayan bir hesap hâlâ bir pozisyon oluşturabilir.
Saldırı adımları analizi
Ön işlemde (0xe1589a19…d60a), saldırgan önce birkaç yardımcı sözleşme oluşturdu ve bu yardımcı sözleşmelere yetki vermek için ERC1155Trade sözleşmesinin setApprovalForAll fonksiyonunu çağırdı; ayrıca iki CashMarket'in vade tarihlerini önceden sorguladı ve sonraki işlemlerde girilecek üç AssetId parametresinin değerlerini hesapladı.

2. Ardından, saldırgan, ERC1155Trade sözleşmesinin safeTransferFrom fonksiyonunu çağırarak, nakit grubu kimliği 2 ve vade zaman damgası 1788480000 (4 Eylül 2026, 08:00) olan 1 birim fCash çifti üretir. Bu işlem, from adresine (saldırgan sözleşmesi) ve to adresine (alıcı sözleşme 1) ait borç ve beklenen gelirleri güncellemek için _upsertAsset iç fonksiyonunu çağırır.

Çift çoğaltıldıktan sonra Portföyler, saldırgan sözleşmesinin serbest teminatını hemen kontrol eder; ancak ilk çoğaltma miktarı çok küçük olduğu için, mevcut döviz kuru ve hassasiyet altında ETH'ye çevrildiğinde sıfıra yuvarlanır, bu nedenle ilk kontrol başarılı olur.
3. Saldırgan, ardından ERC1155Trade sözleşmesinin safeTransferFrom fonksiyonunu tekrar çağırarak, ilgili tahvil varlığının cashGroupId'inin 2 olduğu ancak vade tarihi zaman damgasının 1796256000 (3 Aralık 2026, 08:00) olduğu uint128.max miktarında bir fCash çifti oluşturdu.
Bir detay daha: Saldırgan, farklı vade tarihlerine sahip iki farklı adrese tahviller oluşturdu. Çünkü, from adresindeki borçlar güncellenirken tahvil varlıkları aynıysa doğrudan toplanır ve bu da toplama işlemi sırasında taşma nedeniyle geri döndürülmesine (contract SafeMath kütüphanesini kullanıyor) neden olur.

4. İkinci dövüşten adres (saldırı sözleşmesi) ve to adresi (alıcı sözleşme 2) üzerinden borç ve beklenen gelir güncellendikten sonra, mintfCashPair fonksiyonu, from adresinin net teminat pozisyonunu hesaplamak ve bunun sıfıra eşit veya daha büyük olduğunu kontrol etmek için freeCollateral fonksiyonunu çağırır.

İlk olarak, RiskFramework sözleşmesindeki getRequirement işlevinde, from adresinin iki çoğaltma işlemine ait borç toplamı hesaplanacaktır:

Sonuç -2^128 olur ve bu değer, Escrow sözleşmesinin convertBalancesToETH fonksiyonu aracılığıyla ETH cinsinden dönüştürülür; convertBalancesToETH fonksiyonu, hesaplamaları gerçekleştirmek için ExchangeRate kütüphanesinin _convertToETH fonksiyonunu çağırır:


_convertToETH fonksiyonuna bakıldığında, balance'in önce mutlak değere dönüştürüldükten sonra uint128 kullanılarak 256 bitlik değer 128 bite dönüştürüldüğünü görebiliriz. Yukarıdaki from'un toplam borç miktarı -2^128 olduğundan, mutlak değeri alınan 2^128 değeri uint128() ile zorla dönüştürüldüğünde üst sınırı aşarak 0'a sıçrar. Bu durum, protokolün from adresinin serbest teminat pozisyonunun 0 olduğunu ve bu nedenle sağlıklı olduğunu yanlış bir şekilde kabul etmesine neden olur, böylece mintfCashPair fonksiyonundaki son kontrolü geçer.

5. Ardından Sözleşme 2, pozisyonunu iki yardımcı sözleşmeye böldü; bu iki safeTransferFrom hâlâ mintfCashPair fonksiyonuna girecektir. Ancak bu noktada ödeme yapan, ikinci kez döviz çıkarma sırasında alınan Sözleşme 2'dir ve bu sözleşme, önceki adımda elde edilen büyük beklenti gelir pozisyonuna sahiptir; risk hesaplaması bunu pozitif bir alacak olarak kabul eder ve teminat kontrolünü geçer, bu nedenle iki yardımcı sözleşme, sonraki dönemde nakde dönüştürülebilen receiver fCash kazanır.

6. Saldırgan, daha sonra bir önceki işlemdeki iki konumun ilgili yardımcı sözleşmelerini temizleyerek, karşılık gelen varlıkların nakit bakiyelerini artırıp ardından Escrow sözleşmesinin çek fonksiyonunu çağırarak varlıklarını çekerek kâr elde etti.
Özet
Saldırganın ana stratejisi, iki ayrı pozisyonu kullanarak payer'in toplam borcunu 2^128'e tamamlamak ve ardından Escrow'un bu borcu ETH'ye dönüştürmesini sağlamaktır. Tür dönüşümündeki taşma açıklığını kullanarak sonucu sıfıra keserek risk kontrolünü atlamıştır.
SLOWMAG Güvenlik Ekibi, proje sahiplerinin tür dönüştürme işleminden önce aralık kontrolü yapmalarını ve type(uint128).max değerini aşan sonuçları doğrudan reddetmelerini önerir. Aynı zamanda, imzalı tutarlar, hassasiyet ölçeklemesi ve varlık nominal değerleriyle ilgili tüm sınırlar için uç değer testleri eklenmelidir; özellikle uint128.max, uint128.max + 1 ve negatif sayıların mutlak değerleri gibi girdiler kapsamlı şekilde test edilmelidir. Dönüştürme işlemlerini gerçekleştirmek için Openzeppelin’in SafeCast kütüphanesinden yararlanılabilir.
