8 Ocak 2026'da Ethereum üzerindeki Truebit Protocol istismar edildi ve yaklaşık 26 milyon dolar kayba yol açtı [1]. Temel neden, TRU token satın alma fiyatlandırma mantığındaki bir tam sayı taşmasıydı. Sözleşme, varsayılan olarak taşma kontrollerini zorunlu kılmayan Solidity v0.6.10 ile derlendiğinden, satın alma maliyeti hesaplamasındaki büyük bir ara değer çok daha küçük bir sayıya döndü. Bunun sonucunda, bir saldırgan çok az veya sıfır ETH karşılığında çok büyük miktarda TRU satın alabildi ve ardından edindiği TRU'yu elverişli bir oranda ETH karşılığında sözleşmeye geri satarak protokol rezervlerini boşalttı.
0x0 Arka Plan
Truebit, zincir dışı hesaplama ve etkileşimli doğrulama yoluyla Ethereum için hesaplama hizmetleri sağlar [2]. Protokol içinde TRU token'ları, stake etme ve görev ile ilgili ödemeler dahil olmak üzere teşvikleri koordine etmek için temel ekonomik araç olarak işlev görür.
Protokol, TRU satın almak ve kullanmak için iki genel fonksiyon sunar:
-
buyTRU(), TRU satın alma işlemlerini gerçekleştirir. Gerekli ETH maliyeti,getPurchasePrice()tarafından da kullanılan dahili bir fiyatlandırma fonksiyonu tarafından hesaplanır; bu nedenlegetPurchasePrice(), satın alma işlemi sırasında uygulanan zincir üstü fiyatlandırma mantığını tam olarak yansıtır. -
sellTRU(), TRU satış (kullanım) işlemlerini gerçekleştirir. Beklenen ETH ödemesigetRetirePrice()aracılığıyla sorgulanabilir.
Temel bir tasarım unsuru olan fiyatlandırma asimetrisi şu şekildedir:
- Satın almalar dışbükey bir bağlanma eğrisi kullanır (arz arttıkça marjinal fiyat artar).
- Satışlar doğrusal bir kullanım kuralı kullanır (rezervlerle orantılı).
Uygulama sözleşmesinin kaynak kodu kamuya açık olmadığından, aşağıdaki analiz derlenmiş bayt koduna dayanmaktadır.
Satın alma mantığı
buyTRU() fonksiyonu (ve getPurchasePrice() fonksiyonu), amount kadar TRU satın almak için gereken ETH'yi hesaplayan özel bir _getPurchasePrice() fonksiyonuna fiyatlandırmayı devreder.
function buyTRU(uint256 amount) public payable {
require(msg.data.length - 4 >= 32);
v0 = _getPurchasePrice(amount); // satın alma fiyatını al
require(msg.value == v0, Error('ETH payment does not match TRU order'));
v1 = 0x18ef(100 - _setParameters, msg.value);
v2 = _SafeDiv(100, v1);
v3 = _SafeAdd(v2, _reserve);
_reserve = v3;
require(bool(stor_97_0_19.code.size));
v4 = stor_97_0_19.mint(msg.sender, amount).gas(msg.gas);
require(bool(v4), 0, RETURNDATASIZE()); // çağrı durumunu kontrol eder, hata durumunda hata verisini yayar
return msg.value;
}
function getPurchasePrice(uint256 amount) public nonPayable {
require(msg.data.length - 4 >= 32);
v0 = _getPurchasePrice(amount); // satın alma fiyatını al
return v0;
}
function _getPurchasePrice(uint256 amount) private {
require(bool(stor_97_0_19.code.size));
v0, /* uint256 */ v1 = stor_97_0_19.totalSupply().gas(msg.gas);
require(bool(v0), 0, RETURNDATASIZE()); // çağrı durumunu kontrol eder, hata durumunda hata verisini yayar
require(RETURNDATASIZE() >= 32);
v2 = 0x18ef(v1, v1)
v3 = 0x18ef(_setParameters, v2);
v4 = 0x18ef(v1, v1);
v5 = 0x18ef(100, v4);
v6 = _SafeSub(v3, v5);// payda = 100 * toplamArz**2 - _setParameters * toplamArz**2
v7 = 0x18ef(amount, _reserve);
v8 = 0x18ef(v1, v7);
v9 = 0x18ef(200, v8);// pay_2 = 200 * toplamArz * miktar * _reserve
v10 = 0x18ef(amount, _reserve);
v11 = 0x18ef(amount, v10);
v12 = 0x18ef(100, v11);// pay_1 = 100 * miktar**2 * _reserve
v13 = _SafeDiv(v6, v12 + v9); // satınAlmaFiyatı = (pay_1 + pay_2) / payda
return v13;
}
Derlenen mantıktan, satın alma fiyatı şunların bağlanma eğrisi tarzı bir fonksiyonu olarak ifade edilebilir:
Burada,
- amount: Satın alınacak TRU miktarı
- reserve (_reserve): Sözleşmenin Ether rezervleri
- totalSupply: TRU'nun toplam arzı
- θ (_setParameters): 75'te sabit tutulan bir katsayı
Bu eğri, büyük satın alımları giderek daha pahalı hale getirerek (dışbükey maliyet artışı) spekülasyonu caydırmak ve anlık alım yönlü manipülasyonu azaltmak için tasarlanmıştır.
Satış mantığı
sellTRU() fonksiyonu (ve getRetirePrice() fonksiyonu), TRU kullanıldığında ödenen ETH'yi hesaplamak için özel _getRetirePrice() fonksiyonunu kullanır.
function sellTRU(uint256 amount) public nonPayable {
require(msg.data.length - 4 >= 32);
require(bool(stor_97_0_19.code.size));
v0, /* uint256 */ v1 = stor_97_0_19.allowance(msg.sender, address(this)).gas(msg.gas);
require(bool(v0), 0, RETURNDATASIZE()); // çağrı durumunu kontrol eder, hata durumunda hata verisini yayar
require(RETURNDATASIZE() >= 32);
require(v1 >= amount, Error('Insufficient TRU allowance'));
v2 = _getRetirePrice(amount); // kullanım fiyatını al
v3 = _SafeSub(v2, _reserve);
_reserve = v3;
require(bool(stor_97_0_19.code.size));
v4, /* uint256 */ v5 = stor_97_0_19.transferFrom(msg.sender, address(this), amount).gas(msg.gas);
require(bool(v4), 0, RETURNDATASIZE()); // çağrı durumunu kontrol eder, hata durumunda hata verisini yayar
require(RETURNDATASIZE() >= 32);
require(bool(stor_97_0_19.code.size));
v6 = stor_97_0_19.burn(amount).gas(msg.gas);
require(bool(v6), 0, RETURNDATASIZE()); // çağrı durumunu kontrol eder, hata durumunda hata verisini yayar
v7 = msg.sender.call().value(v2).gas(!v2 * 2300);
require(bool(v7), 0, RETURNDATASIZE()); // çağrı durumunu kontrol eder, hata durumunda hata verisini yayar
return v2;
}
function getRetirePrice(uint256 amount) public nonPayable {
require(msg.data.length - 4 >= 32);
v0 = _getRetirePrice(amount); // kullanım fiyatını al
return v0;
}
function _getRetirePrice(uint256 amount) private {
require(bool(stor_97_0_19.code.size));
v0, /* uint256 */ v1 = stor_97_0_19.totalSupply().gas(msg.gas);
require(bool(v0), 0, RETURNDATASIZE()); // çağrı durumunu kontrol eder, hata durumunda hata verisini yayar
require(RETURNDATASIZE() >= 32);
v1 = v2.length;
v3 = v2.data;
v4 = 0x18ef(_reserve, amount);// pay = _reserve * miktar
if (v1 > 0) {
assert(v1);
return v4 / v1;// kullanımFiyatı = pay / toplamArz
} else {
// ...
}
Kullanım kuralı doğrusaldır:
Kullanım fiyatı, kullanılan toplam arzın oranıyla (yani miktar / toplamArz) çarpılan reserve ile orantılıdır.
Bu kasıtlı asimetri, geniş bir spread oluşturur: satın alma işlemi dışbükeydir (büyük ölçekte pahalıdır), satış ise doğrusaldır (yalnızca rezervlerin orantılı bir payını kullanır). Normal koşullar altında bu spread, anlık al→sat arbitrajını cazip olmaktan çıkarır.
0x1 Güvenlik Açığı Analizi
Büyük alımlar pahalıdır şeklindeki tasarım amacına karşın, _getPurchasePrice(), aritmetiğinde bir tam sayı taşması içermektedir. Sözleşme Solidity 0.6.10 ile derlendiğinden, uint256 üzerindeki aritmetik işlemler, açıkça korunmadığı sürece (ör. SafeMath aracılığıyla) sessizce taşabilir ve 2^256 modulo çevrilebilir.
function _getPurchasePrice(uint256 amount) private {
require(bool(stor_97_0_19.code.size));
v0, /* uint256 */ v1 = stor_97_0_19.totalSupply().gas(msg.gas);
require(bool(v0), 0, RETURNDATASIZE()); // çağrı durumunu kontrol eder, hata durumunda hata verisini yayar
require(RETURNDATASIZE() >= 32);
v2 = 0x18ef(v1, v1)
v3 = 0x18ef(_setParameters, v2);
v4 = 0x18ef(v1, v1);
v5 = 0x18ef(100, v4);
v6 = _SafeSub(v3, v5);// payda = 100 * toplamArz**2 - _setParameters * toplamArz**2
v7 = 0x18ef(amount, _reserve);
v8 = 0x18ef(v1, v7);
v9 = 0x18ef(200, v8);// pay_2 = 200 * toplamArz * miktar * _reserve
v10 = 0x18ef(amount, _reserve);
v11 = 0x18ef(amount, v10);
v12 = 0x18ef(100, v11);// pay_1 = 100 * miktar**2 * _reserve
v13 = _SafeDiv(v6, v12 + v9); // satınAlmaFiyatı = (pay_1 + pay_2) / payda
return v13;
}
_getPurchasePrice() içinde, yeterince büyük bir amount değeri, iki büyük pay teriminin toplanması sırasında taşmayı tetikler (derlenen kod parçasında v12 + v9). Bu taşma meydana geldiğinde, pay küçük bir değere döner ve bu da nihai bölme işleminin yapay olarak düşük bir satın alma fiyatı, potansiyel olarak sıfır döndürmesine yol açar.
Kritik olarak, taşma yalnızca alım tarafı fiyatlandırmasını etkiler. Satış tarafı fonksiyonu doğrusal olmaya devam eder ve amaçlandığı gibi davranır; bu nedenle bir saldırgan şunları yapabilir:
- Düşük fiyatlı (veya sıfır maliyetli) büyük miktarda TRU satın alabilir, ardından
sellTRU()aracılığıyla çok daha yüksek efektif bir oranda ETH karşılığında kullanabilir.
0x2 Saldırı Analizi
Saldırgan, tek bir işlem içinde birden fazla arbitraj turu gerçekleştirdi [3] ve şu adımları tekrarladı: getPurchasePrice() -> buyTRU() -> sellTRU()
İlk tur: sıfır maliyetli satın alma, ardından kârla satış
Dikkatle seçilmiş bir satın alma miktarı (240.442.509.453.545.333.947.284.131) sağlayan saldırgan, _getPurchasePrice() içinde taşmayı tetikledi, hesaplanan satın alma fiyatını 0 ETH'ye düşürdü ve ~240 milyon TRU'yu sıfır maliyetle elde etmesini sağladı.
Aşağıdaki Python kodu kontrolü, payın 2^256'yı aştığını ve döndükten sonra hesaplanan satın alma fiyatının tam sayıya dönüştürüldüğünde sıfıra truncate olan küçük bir kesirli değer haline geldiğini göstermektedir.
>>> _reserve = 0x1ceec1aef842e54d9ee
>>> totalSupply = 161753242367424992669183203
>>> amount = 240442509453545333947284131
>>> numerator = int(100 * amount * _reserve * (amount + 2 * totalSupply))
>>> numerator > 2**256
True
>>> denominator = (100 - 75) * totalSupply**2
>>> purchasePrice = (numerator - 2**256) / denominator
>>> purchasePrice
0.00025775798757211426
>>> int(purchasePrice)
0
Saldırgan daha sonra hemen sellTRU()'yu çağırdı ve TRU'yu protokol rezervlerinden 5.105 ETH karşılığında kullandı.
Sonraki turlar: düşük maliyetli satın almalar, ardından kârla satış
Saldırgan döngüyü birden fazla kez tekrarladı. Sonraki satın almalar her zaman kesin olarak sıfır maliyetli olmadı, ancak taşma satın alma fiyatlarını karşılık gelen satış getirilerinin çok altında tutmaya devam etti.
Genel olarak saldırgan, Truebit'in rezervlerinden 8.535 ETH boşalttı.
0x3 Özet
Bu olay nihayetinde Truebit'in alım tarafı fiyatlandırma mantığındaki denetlenmemiş tam sayı taşmasından kaynaklandı. Protokolün asimetrik alım/satım fiyatlandırma modelinin spekülatörlere karşı koymak amacıyla tasarlanmış olmasına karşın, sistematik taşma koruması olmaksızın eski bir Solidity sürümüyle (0.8 öncesi) derleme yapılması tasarımı zayıflattı ve rezervlerin boşaltılmasını mümkün kıldı.
Hâlâ 0.8'in altındaki Solidity sürümlerini kullanan herhangi bir üretim sözleşmesi için geliştiriciler şunları yapmalıdır:
- Her ilgili işleme taşmaya karşı güvenli aritmetik uygulayın (ör.
SafeMathveya eşdeğer kontroller), ya da - Varsayılan taşma kontrollerinden yararlanmak için tercihen Solidity 0.8+'e geçiş yapın.
Referans
[1] https://x.com/Truebitprotocol/status/2009328032813850839
[2] https://docs.truebit.io/v1docs
[3] Saldırı işlemi
BlockSec Hakkında
BlockSec, tam kapsamlı bir blok zinciri güvenlik 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 önlemesine, olayları analiz etmesine, yasadışı fonları takip etmesine ve protokoller ile platformların tam yaşam döngüsü boyunca AML/CFT yükümlülüklerini yerine getirmesine yardımcı olan ürün ve hizmetler oluşturuyoruz.
BlockSec, prestijli konferanslarda birden fazla blok zinciri güvenlik makalesi yayımlamış, DeFi uygulamalarının çeşitli sıfır gün saldırılarını raporlamış, 20 milyonun üzerinde doları kurtarmak için birden fazla saldırıyı engellemiş ve milyarlarca dolar değerindeki kripto para birimini güvence altına almıştır.
-
Resmi web sitesi: https://blocksec.com/
-
Resmi Twitter hesabı: https://twitter.com/BlockSecTeam



