DeFi AMM Accounting Bugs & Virtual Balance Cache Exploitation
Tip
AWS Hacking’i öğrenin ve pratik yapın:
HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın:HackTricks Training GCP Red Team Expert (GRTE)
Azure Hacking’i öğrenin ve pratik yapın:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks'i Destekleyin
- abonelik planlarını kontrol edin!
- 💬 Discord grubuna veya telegram grubuna katılın ya da Twitter’da bizi takip edin 🐦 @hacktricks_live.**
- Hacking ipuçlarını paylaşmak için HackTricks ve HackTricks Cloud github reposuna PR gönderin.
Genel Bakış
Yearn Finance’ın yETH havuzu (Kasım 2025), karmaşık AMM’lerde gaz tasarrufu amaçlı önbelleklerin sınır-durumu geçişleri sırasında uzlaştırılmadığında nasıl silah haline getirilebileceğini gösterdi. Ağırlıklı stableswap havuzu 32’ye kadar liquid staking derivative (LSD) izliyor, bunları ETH-eşdeğerine çeviriyor — virtual balances (vb_i = balance_i × rate_i / PRECISION) — ve bu değerleri packed_vbs[] adlı paketlenmiş bir storage dizisinde saklıyordu. Tüm LP token’ları yakıldığında totalSupply doğru şekilde sıfıra düşse de cached packed_vbs[i] slotları büyük tarihsel değerleri tutmaya devam etti. Ardından gelen depositor, önbellek hâlâ hayalet likidite içeriyor olmasına rağmen “ilk” liquidity provider olarak kabul edildi; bu da bir saldırganın sadece 16 wei karşılığında ≈235 septilyon yETH mint etmesine ve ≈USD 9M değerindeki LSD teminatını boşaltmasına izin verdi.
Temel bileşenler:
- Derived-state caching: pahalı oracle sorgularından kaçınmak için virtual balances saklanır ve artımlı olarak güncellenir.
- Missing reset when supply == 0: remove_liquidity() orantılı azalmalar sonrası packed_vbs[] içinde sıfır olmayan kalıntılar bıraktı.
- Initialization branch trusts the cache: add_liquidity() _calc_vb_prod_sum() çağırır ve prev_supply == 0 olduğunda packed_vbs[]’i sadece okur, önbelleğin de sıfırlandığını varsayar.
- Flash-loan financed state poisoning: deposit/withdraw döngüleri yuvarlama kalıntılarını sermaye kilidi olmadan büyütüp “first deposit” yolunda yıkıcı bir over-mint’e olanak sağladı.
Önbellek tasarımı ve eksik sınır durumu işleme
Zayıf akış aşağıda basitleştirilmiştir:
function remove_liquidity(uint256 burnAmount) external {
uint256 supplyBefore = totalSupply();
_burn(msg.sender, burnAmount);
for (uint256 i; i < tokens.length; ++i) {
packed_vbs[i] -= packed_vbs[i] * burnAmount / supplyBefore; // truncates to floor
}
// BUG: packed_vbs not cleared when supply hits zero
}
function add_liquidity(Amounts calldata amountsIn) external {
uint256 prevSupply = totalSupply();
uint256 sumVb = prevSupply == 0 ? _calc_vb_prod_sum() : _calc_adjusted_vb(amountsIn);
uint256 lpToMint = pricingInvariant(sumVb, prevSupply, amountsIn);
_mint(msg.sender, lpToMint);
}
function _calc_vb_prod_sum() internal view returns (uint256 sum) {
for (uint256 i; i < tokens.length; ++i) {
sum += packed_vbs[i]; // assumes cache == 0 for a pristine pool
}
}
Because remove_liquidity() only applied proportional decrements, every loop left sabit nokta yuvarlama artıkları. After ≳10 deposit/withdraw cycles those residues accumulated into extremely large phantom virtual balances while the on-chain token balances were almost empty. Burning the final LP shares set totalSupply to zero yet caches stayed populated, priming the protocol for a malformed initialization.
Sömürü oyun kitabı (yETH vaka çalışması)
- Flash-loan working capital – Balancer/Aave’ten wstETH, rETH, cbETH, ETHx, WETH vb. ödünç alarak havuzu manipüle ederken sermayeyi bağlamaktan kaçının.
- Poison
packed_vbs[]– Sekiz LSD varlığı arasında deposit/withdraw döngüleri yapın. Her kısmi çekimpacked_vbs[i] − vb_sharedeğerini kırparak token başına >0 artık bırakır. Döngüyü tekrarlamak, gerçek bakiyeler kabaca dengelenirken hayalet ETH-eşdeğer bakiyeleri şişirir. - Force
supply == 0– Havuzun boş olduğuna inanması için kalan tüm LP token’ları yakın. Uygulamadaki göz ardı edilen hata zehirlenmişpacked_vbs[]’i dokunulmadan bırakır. - Dust-size “first deposit” – Desteklenen LSD slotlarına bölünmüş toplam 16 wei gönderin.
add_liquidity()prev_supply == 0olduğunu görür,_calc_vb_prod_sum()çalıştırılır ve gerçek bakiyelerden yeniden hesaplamak yerine bayat cache’i okur. Bu yüzden mint hesabı trilyonlarca USD girmiş gibi davranır ve ~2.35×10^26 yETH basar. - Drain & repay – Şişirilmiş LP pozisyonunu tüm vault’lanmış LSD’ler için itfa edin, yETH→WETH’i Balancer’da takas edin, Uniswap v3 ile ETH’ye çevirin, flash loan/ücretleri geri ödeyin ve kârı aklayın (ör. Tornado Cash aracılığıyla). Net kâr ≈USD 9M; havuza temas eden kendi fon yalnızca 16 wei idi.
Genelleştirilmiş sömürü koşulları
Benzer AMM’leri aşağıdaki koşulların tümü sağlandığında suistimal edebilirsiniz:
- Cached derivatives of balances (virtual balances, TWAP snapshots, invariant helpers) gaz tasarrufu için işlemler arasında kalıcı tutuluyor.
- Partial updates truncate sonuçları (floor division, fixed-point rounding), saldırganın simetrik deposit/withdraw döngüleriyle durumlu artıkları biriktirmesine izin veriyor.
- Boundary conditions reuse caches yerine gerçek yeniden hesaplama yapılmıyor; özellikle
totalSupply == 0,totalLiquidity == 0veya havuz bileşimi sıfırlandığında. - Minting logic lacks ratio sanity checks (ör.
expected_value/actual_valuesınırlarının olmaması), böylece bir toz depozit neredeyse tüm tarihsel arzı basabiliyor. - Cheap capital is available (flash loans veya iç kredi) tek bir işlem içinde veya sıkı koordine edilmiş paketlerle düzinelerce durum-düzenleyici işlemi çalıştırmaya olanak veriyor.
Savunma mühendisliği kontrol listesi
- Explicit resets when supply/lpShares hit zero:
if (totalSupply == 0) {
for (uint256 i; i < tokens.length; ++i) packed_vbs[i] = 0;
}
Bakiyelerden veya oracle verilerinden türetilmiş her önbelleğe alınmış birikiciye aynı muameleyi uygulayın.
- Recompute on initialization branches –
prev_supply == 0durumunda cache’leri tamamen yoksayın ve virtual balances’ı gerçek token bakiyeleri + canlı oracle oranlarından yeniden inşa edin. - Minting sanity bounds –
lpToMint > depositValue × MAX_INIT_RATIOise revert edin veya tek bir işlemde tarihsel arzın >X% basılmasına izin vermeyin; toplam depozitler minimum eşik altındaysa işlemi reddedin. - Rounding-residue drains – Token başına yuvarlama artığını bir sink’e (treasury/burn) toplayın, böylece tekrarlanan orantılı ayarlamalar cache’leri gerçek bakiyelerden uzaklaştırmaz.
- Differential tests – Her durum geçişi (add/remove/swap) için aynı invariant’ı off-chain, yüksek hassasiyetli matematikle yeniden hesaplayın ve tam likidite boşalmalarından sonra bile sıkı bir epsilon içinde eşitliği doğrulayın.
İzleme ve müdahale
- Multi-transaction detection – Havuzu düşük bakiye ama yüksek cache durumuyla bırakan ve ardından
supply == 0ile sonuçlanan neredeyse simetrik deposit/withdraw dizilerini izleyin. Tek işlemli anomali tespitçileri bu zehirleme kampanyalarını gözden kaçırır. - Runtime simulations –
add_liquidity()çalıştırılmadan önce virtual balances’ı sıfırdan yeniden hesaplayın ve cached toplamlarla karşılaştırın; farklar baz puan eşiğini aşarsa revert veya pause uygulayın. - Flash-loan aware alerts – Büyük flash loan’lar, kapsamlı havuz çekimleri ve toz büyüklüğünde bir son depoziti birleştiren işlemleri işaretleyin; bunları engelleyin veya manuel onay gerektirin.
References
Tip
AWS Hacking’i öğrenin ve pratik yapın:
HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın:HackTricks Training GCP Red Team Expert (GRTE)
Azure Hacking’i öğrenin ve pratik yapın:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks'i Destekleyin
- abonelik planlarını kontrol edin!
- 💬 Discord grubuna veya telegram grubuna katılın ya da Twitter’da bizi takip edin 🐦 @hacktricks_live.**
- Hacking ipuçlarını paylaşmak için HackTricks ve HackTricks Cloud github reposuna PR gönderin.


